diff --git a/.env.local b/.env.local index 8ccdefd9048..cb6d89c9673 100644 --- a/.env.local +++ b/.env.local @@ -83,14 +83,14 @@ NATIVE_LOGIN_ENABLED=YES # Fast login max retries FAST_LOGIN_MAX_RETRIES=3 # Fast login opt-in -FAST_LOGIN_OPTIN=NO +FAST_LOGIN_OPTIN=YES # Enable CIE login flow with emulator and dev server CIE_LOGIN_WITH_DEV_SERVER_ENABLED=NO -# Enable CDU flow new screen -CDU_NEW_FLOW=NO # Relay State for SPID SPID_RELAY_STATE='appio-dev' # Wallet V3 RESTful API WALLET_API_BASEURL='http://127.0.0.1:3000' # Wallet V3 test/env RESTful API WALLET_API_UAT_BASEURL='https://api.uat.platform.pagopa.it' +# Redesign of the services section +NEW_SERVICES_ENABLED=NO \ No newline at end of file diff --git a/.env.production b/.env.production index 964c21771b0..07db02cf403 100644 --- a/.env.production +++ b/.env.production @@ -86,11 +86,11 @@ FAST_LOGIN_MAX_RETRIES=3 FAST_LOGIN_OPTIN=YES # Enable CIE login flow with emulator and dev server CIE_LOGIN_WITH_DEV_SERVER_ENABLED=NO -# Enable CDU flow new screen -CDU_NEW_FLOW=YES # Relay State for SPID SPID_RELAY_STATE='appio' # Wallet RESTful API WALLET_API_BASEURL='https://api.platform.pagopa.it' # Wallet test/env RESTful API WALLET_API_UAT_BASEURL='https://api.uat.platform.pagopa.it' +# Redesign of the services section +NEW_SERVICES_ENABLED=NO diff --git a/.github/actions/setup-composite/action.yml b/.github/actions/setup-composite/action.yml index 705495bf838..bcf2e107204 100644 --- a/.github/actions/setup-composite/action.yml +++ b/.github/actions/setup-composite/action.yml @@ -17,11 +17,7 @@ runs: with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - id: install-packages run: yarn install --frozen-lockfile shell: bash diff --git a/.github/scripts/run-e2e-tests.sh b/.github/scripts/run-e2e-tests.sh index 9a2c01b2b31..7217ccb2aad 100644 --- a/.github/scripts/run-e2e-tests.sh +++ b/.github/scripts/run-e2e-tests.sh @@ -1,10 +1,21 @@ +#!/bin/sh + +# Check if the correct number of arguments is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Navigate to the API server directory and start setup cd _io-dev-api-server_ cp ../scripts/api-config.json config/config.json yarn yarn generate yarn start &> /tmp/io-dev-api-server.log & + # wait for the server to be up and running sleep 10 + cd .. yarn detox clean-framework-cache yarn detox build-framework-cache @@ -17,4 +28,4 @@ yarn detox test \ --take-screenshots all \ --record-videos failing \ --debug-synchronization 1000 \ - --retries 3 \ No newline at end of file + --retries 3 "$1" \ No newline at end of file diff --git a/.github/workflows/release-fl.yml b/.github/workflows/release-fl.yml index 56bb3da4a7e..29be9a64d1b 100644 --- a/.github/workflows/release-fl.yml +++ b/.github/workflows/release-fl.yml @@ -46,10 +46,10 @@ jobs: release-ios: needs: run-static-checks environment: prod - runs-on: macos-12 + runs-on: macos-13-xlarge steps: - id: set-xcode-version - run: sudo xcode-select -s '/Applications/Xcode_14.2.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' shell: bash - id: checkout uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab #v3.5.2 @@ -58,7 +58,7 @@ jobs: - id: setup uses: ./.github/actions/setup-composite - id: setup-ruby - uses: ruby/setup-ruby@d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c #v1.149.0 + uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899 #v1.173.0 with: bundler-cache: true - id: prepare-ios-build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62f1e5aaf5e..50536ccf973 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,10 +47,10 @@ jobs: release-ios: needs: run-static-checks environment: prod - runs-on: macos-12 + runs-on: macos-13-xlarge steps: - id: set-xcode-version - run: sudo xcode-select -s '/Applications/Xcode_14.2.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' shell: bash - id: checkout uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab #v3.5.2 @@ -59,7 +59,7 @@ jobs: - id: setup uses: ./.github/actions/setup-composite - id: setup-ruby - uses: ruby/setup-ruby@d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c #v1.149.0 + uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899 #v1.173.0 with: bundler-cache: true - id: prepare-ios-build @@ -90,6 +90,17 @@ jobs: APP_STORE_API_KEY_ISSUER_ID: ${{secrets.APP_STORE_API_KEY_ISSUER_ID}} ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD: ${{secrets.ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD}} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + - id: upload-dsym-files + # Sometimes the build-upload-app-store step fails for timeout, + # in this case we want to upload the dSYM files anyway + if: ${{ always() }} + # We don't want to fail whole job if the dSYM upload step fails + continue-on-error: true + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.0.3 + with: + name: ItaliaApp.app.dSYM.zip + path: ios/ItaliaApp.app.dSYM.zip + retention-days: 60 notify-new-version: environment: dev runs-on: ubuntu-latest diff --git a/.github/workflows/staticcheck.yaml b/.github/workflows/staticcheck.yaml index 6cda17b269b..28e71a1b417 100644 --- a/.github/workflows/staticcheck.yaml +++ b/.github/workflows/staticcheck.yaml @@ -21,4 +21,4 @@ jobs: - id: run-test run: yarn test:ci - id: codecov-script - run: ./scripts/codecov.sh \ No newline at end of file + run: ./scripts/codecov.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index c49ae1f34ad..59a4294e144 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,25 +1,27 @@ name: Run e2e tests on: + workflow_dispatch: push: branches: - master jobs: run-static-checks: - uses: ./.github/workflows/staticcheck.yaml - run-e2e-test-ios: + uses: ./.github/workflows/staticcheck.yaml + build-detox-app: needs: run-static-checks - runs-on: macos-latest - environment: dev + runs-on: macos-12 concurrency: - group: ${{ github.workflow }}-e2e-tests-${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-e2e-tests-${{ github.ref || github.run_id }} cancel-in-progress: true steps: - id: checkout uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - id: setup uses: ./.github/actions/setup-composite + with: + use-cache: 'true' - id: setup-ruby - uses: ruby/setup-ruby@d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c # v1.148.0 + uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899 #v1.173.0 with: bundler-cache: true - id: prepare-dependencies @@ -32,9 +34,7 @@ jobs: with: path: ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - id: install-detox + - id: install-applesimutils run: | brew tap wix/brew brew install applesimutils @@ -46,29 +46,70 @@ jobs: with: path: ios/build key: ${{ runner.os }}-detox-build - restore-keys: | - ${{ runner.os }}-detox-build - id: setup-pods run: cd ios ; bundle exec pod install --verbose ; cd .. shell: bash - id: prepare-detox-build run: RN_SRC_EXT=e2e.ts yarn detox build -c ios.sim.release shell: bash + - id: upload-detox-build + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.0.3 + with: + name: ItaliaApp.app + path: ios/build/Build/Products/Release-iphonesimulator/ItaliaApp.app + retention-days: 2 + run-e2e-tests: + needs: build-detox-app + runs-on: macos-latest + environment: dev + strategy: + fail-fast: false + matrix: + include: + - test: "ts/__e2e__/" + name: "Base_e2e" + - test: "ts/features/bonus/cgn/__e2e__/" + name: "cgn_e2e" + - test: "ts/features/messages/__e2e__/" + name: "messages_e2e" + - test: "ts/features/euCovidCert/__e2e__/" + name: "eucovidcert_e2e" + - test: "ts/features/wallet/onboarding/__e2e__/" + name: "wallet_onboarding_e2e" + steps: + - id: install-applesimutils + run: | + brew tap wix/brew + brew install applesimutils + shell: bash + - id: checkout + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - id: download-detox-build + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + with: + name: ItaliaApp.app + path: ios/build/Build/Products/Release-iphonesimulator/ItaliaApp.app + - id: setup + uses: ./.github/actions/setup-composite + with: + use-cache: 'true' - id: checkout-dev-server uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: repository: pagopa/io-dev-api-server path: './_io-dev-api-server_' - id: run-e2e-tests - run: bash ./.github/scripts/run-e2e-tests.sh + run: bash ./.github/scripts/run-e2e-tests.sh ${{ matrix.test }} - id: notify-test-failure if: failure() uses: ./.github/actions/notify-e2e env: + TEST: ${{ matrix.name }} IO_APP_SLACK_HELPER_BOT_TOKEN: ${{ secrets.IO_APP_SLACK_HELPER_BOT_TOKEN }} - id: upload-artifacts - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v.3.1.2 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v.3.1.2 if: always() with: - name: detox-artifacts - path: /tmp/e2e_artifacts/ \ No newline at end of file + name: detox-artifacts-${{ matrix.name }} + path: /tmp/e2e_artifacts/ + retention-days: 2 diff --git a/.gitignore b/.gitignore index c880c3a4cab..2a5b814922d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ local.properties *.iml *.hprof Google Play Android Developer-04750d2c0cf8.json +.cxx/ **/fastlane/report.xml **/fastlane/Preview.html diff --git a/.hound.yml b/.hound.yml deleted file mode 100644 index 74b81cf446d..00000000000 --- a/.hound.yml +++ /dev/null @@ -1,4 +0,0 @@ -tslint: - enabled: true - config_file: tslint.json - diff --git a/.node-version b/.node-version index e65243f2ea3..a9d087399d7 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -16.19.0 +18.19.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ea94b888d..b9744fdb9bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,342 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.58.0-rc.0](https://github.com/pagopa/io-app/compare/2.57.0-rc.1...2.58.0-rc.0) (2024-04-09) + + +### Features + +* [[IOCOM-1199](https://pagopa.atlassian.net/browse/IOCOM-1199)] Vertically ordered CTAs and Payment button on new DS message details screen ([#5636](https://github.com/pagopa/io-app/issues/5636)) ([0fa34fb](https://github.com/pagopa/io-app/commit/0fa34fb1c76ab5b11e87f2aed5dfdf5ff0d82929)) +* [[IOCOM-870](https://pagopa.atlassian.net/browse/IOCOM-870)] New DS on payment list in the SEND message details ([#5650](https://github.com/pagopa/io-app/issues/5650)) ([1f31acb](https://github.com/pagopa/io-app/commit/1f31acb4c1d7a4348b7504bf35affc99f82b752e)) +* [[IOCOM-874](https://pagopa.atlassian.net/browse/IOCOM-874)] Bottom menu on SEND message details, with "Support" action item ([#5644](https://github.com/pagopa/io-app/issues/5644)) ([9f72690](https://github.com/pagopa/io-app/commit/9f72690a37c11038935afb2ce40ec82051f94355)) + + +### Chores + +* **Cross:** [[IOAPPX-271](https://pagopa.atlassian.net/browse/IOAPPX-271)] Delete components and assets no longer referenced ([#5646](https://github.com/pagopa/io-app/issues/5646)) ([fe14f08](https://github.com/pagopa/io-app/commit/fe14f08e597b4a943cdd8aa1b4f8aab509e13dd7)) +* **deps:** bump react-native-document-picker from 9.0.1 to 9.1.1 ([#5520](https://github.com/pagopa/io-app/issues/5520)) ([cb1af3e](https://github.com/pagopa/io-app/commit/cb1af3e236fc2e2826aa8e6a87adf01c65c1996c)), closes [rnmods/react-native-document-picker#698](https://github.com/rnmods/react-native-document-picker/issues/698) [rnmods/react-native-document-picker#689](https://github.com/rnmods/react-native-document-picker/issues/689) [rnmods/react-native-document-picker#654](https://github.com/rnmods/react-native-document-picker/issues/654) [rnmods/react-native-document-picker#660](https://github.com/rnmods/react-native-document-picker/issues/660) [rnmods/react-native-document-picker#666](https://github.com/rnmods/react-native-document-picker/issues/666) [rnmods/react-native-document-picker#667](https://github.com/rnmods/react-native-document-picker/issues/667) [rnmods/react-native-document-picker#670](https://github.com/rnmods/react-native-document-picker/issues/670) [rnmods/react-native-document-picker#686](https://github.com/rnmods/react-native-document-picker/issues/686) [rnmods/react-native-document-picker#687](https://github.com/rnmods/react-native-document-picker/issues/687) [rnmods/react-native-document-picker#689](https://github.com/rnmods/react-native-document-picker/issues/689) [#698](https://github.com/pagopa/io-app/issues/698) [#689](https://github.com/pagopa/io-app/issues/689) [#687](https://github.com/pagopa/io-app/issues/687) [#686](https://github.com/pagopa/io-app/issues/686) [#670](https://github.com/pagopa/io-app/issues/670) [#667](https://github.com/pagopa/io-app/issues/667) [#666](https://github.com/pagopa/io-app/issues/666) [#660](https://github.com/pagopa/io-app/issues/660) +* **IT Wallet:** [[SIW-951](https://pagopa.atlassian.net/browse/SIW-951)] Add user payment methods in new wallet section ([#5639](https://github.com/pagopa/io-app/issues/5639)) ([86087cc](https://github.com/pagopa/io-app/commit/86087cc684685f5e6aa650201fd7bbf9645b1078)) +* **IT Wallet:** [[SIW-963](https://pagopa.atlassian.net/browse/SIW-963)] Add wallet cards onboarding screen ([#5648](https://github.com/pagopa/io-app/issues/5648)) ([902faae](https://github.com/pagopa/io-app/commit/902faae6b4f8ef8c47065abbd002dfa716acc8ed)) +* [[IOBP-615](https://pagopa.atlassian.net/browse/IOBP-615)] Add new `paymentCard` component to `PaymentsMethodDetailsScreen` ([#5665](https://github.com/pagopa/io-app/issues/5665)) ([afb6fd6](https://github.com/pagopa/io-app/commit/afb6fd6141260ec5caa94d3bda97b4b5f09c29e5)) +* [[IOPID-1720](https://pagopa.atlassian.net/browse/IOPID-1720)] Add features to OperationResultScreenContent ([#5666](https://github.com/pagopa/io-app/issues/5666)) ([4e9a9a9](https://github.com/pagopa/io-app/commit/4e9a9a9cabd389531b59e03db5b4f3e49973e5d2)) + +## [2.57.0-rc.1](https://github.com/pagopa/io-app/compare/2.57.0-rc.0...2.57.0-rc.1) (2024-04-04) + + +### Features + +* [[IOCOM-1172](https://pagopa.atlassian.net/browse/IOCOM-1172)] Update business logic for payment reminder on a message details screen with the new DS ([#5613](https://github.com/pagopa/io-app/issues/5613)) ([2a34b40](https://github.com/pagopa/io-app/commit/2a34b40a9eba194f8716824364d87d3610a77c2d)) +* [[IOCOM-873](https://pagopa.atlassian.net/browse/IOCOM-873)] IUN code on SEND message details with new DS ([#5643](https://github.com/pagopa/io-app/issues/5643)) ([4df62f9](https://github.com/pagopa/io-app/commit/4df62f9ca0e68987f03b841176dc6d736631df89)) +* [[IOCOM-882](https://pagopa.atlassian.net/browse/IOCOM-882)] New DS on a paid payment screen for a cancelled PN message ([#5621](https://github.com/pagopa/io-app/issues/5621)) ([6658689](https://github.com/pagopa/io-app/commit/6658689b8e4fd5ae592427d479d104ffaf854561)) +* [[IOPID-1541](https://pagopa.atlassian.net/browse/IOPID-1541),[IOPID-1542](https://pagopa.atlassian.net/browse/IOPID-1542)] Adopt the new DS to `RootedModal` ([#5641](https://github.com/pagopa/io-app/issues/5641)) ([8de52b5](https://github.com/pagopa/io-app/commit/8de52b5f2e2b10bca2290a728f0c50bf857c7d3f)) +* [[IOPID-1611](https://pagopa.atlassian.net/browse/IOPID-1611)] Add en copy on modal cie not supported ([#5652](https://github.com/pagopa/io-app/issues/5652)) ([5178c00](https://github.com/pagopa/io-app/commit/5178c005ab470f9a099425148de9323148243c9c)) +* [[IOPID-1611](https://pagopa.atlassian.net/browse/IOPID-1611)] Adopt new DS on CIE not supported modal ([#5647](https://github.com/pagopa/io-app/issues/5647)) ([65dfab0](https://github.com/pagopa/io-app/commit/65dfab0c201f93c52aef2530a949c48c4ce91540)) + + +### Bug Fixes + +* [[IOPID-1714](https://pagopa.atlassian.net/browse/IOPID-1714)] IOS crashes on EIC malformed URLs ([#5660](https://github.com/pagopa/io-app/issues/5660)) ([9c640a4](https://github.com/pagopa/io-app/commit/9c640a401d5c1c59c4c37d72e6fca544e18ad23f)) +* cgn test failing for new services FF ([#5649](https://github.com/pagopa/io-app/issues/5649)) ([5a78655](https://github.com/pagopa/io-app/commit/5a786552cf99ffd4b5a0a44579f3e8480c6a62c7)) +* Translations on Services Preference Screens and Identification Modal ([#5653](https://github.com/pagopa/io-app/issues/5653)) ([319706e](https://github.com/pagopa/io-app/commit/319706e8112ca67f8b86e3f74bcae10acda8bbe9)) + + +### Chores + +* [[IOBP-576](https://pagopa.atlassian.net/browse/IOBP-576)] Payment manual input A11y and UX fixes ([#5556](https://github.com/pagopa/io-app/issues/5556)) ([c4ee44c](https://github.com/pagopa/io-app/commit/c4ee44cd9f5a91531eb7de71ebb3e6bb065de84c)) +* [[IOBP-607](https://pagopa.atlassian.net/browse/IOBP-607)] Fix payments landing screen scrollview behavior ([#5656](https://github.com/pagopa/io-app/issues/5656)) ([03940ba](https://github.com/pagopa/io-app/commit/03940baef962d4af843726f8e804217cbad61205)) +* [[IOBP-610](https://pagopa.atlassian.net/browse/IOBP-610)] Refactor `payments` feature ([#5654](https://github.com/pagopa/io-app/issues/5654)) ([cc7aabe](https://github.com/pagopa/io-app/commit/cc7aabee62a53816854f9f6560be86005f651f08)) +* [[IOPAE-1046](https://pagopa.atlassian.net/browse/IOPAE-1046)] Add Service header to `ServiceDetailsScreen` ([#5633](https://github.com/pagopa/io-app/issues/5633)) ([6e6bb73](https://github.com/pagopa/io-app/commit/6e6bb73fedeb75dba83dc736407a4fd8abfe7f31)) +* [[IOPAE-1047](https://pagopa.atlassian.net/browse/IOPAE-1047)] Add service description to `ServiceDetailsScreen` ([#5640](https://github.com/pagopa/io-app/issues/5640)) ([8fbdc80](https://github.com/pagopa/io-app/commit/8fbdc8048c15aabd0d9f92824ab1f63d602548d2)) +* [[IOPAE-1066](https://pagopa.atlassian.net/browse/IOPAE-1066)] Service details failure state ([#5642](https://github.com/pagopa/io-app/issues/5642)) ([94271fc](https://github.com/pagopa/io-app/commit/94271fc149c100fd273994d8d9767bd72165ecf0)) +* **IT Wallet:** [[SIW-962](https://pagopa.atlassian.net/browse/SIW-962)] Add cards onboarding CTA in the new wallet section ([#5645](https://github.com/pagopa/io-app/issues/5645)) ([4a25d67](https://github.com/pagopa/io-app/commit/4a25d67d266c3ef8f0ecd319b619ce4fa33dfc82)) + +## [2.57.0-rc.0](https://github.com/pagopa/io-app/compare/2.56.0-rc.3...2.57.0-rc.0) (2024-03-28) + + +### Features + +* [[IOCOM-872](https://pagopa.atlassian.net/browse/IOCOM-872)] Base F24 section with new DS for SEND messages ([#5595](https://github.com/pagopa/io-app/issues/5595)) ([10a2f27](https://github.com/pagopa/io-app/commit/10a2f276b1e6fc7673c36af2a1c3fc92fe9806a0)) +* [[IOCOM-879](https://pagopa.atlassian.net/browse/IOCOM-879)] Bottom sheet for SEND's F24 with the new DS ([#5603](https://github.com/pagopa/io-app/issues/5603)) ([7271904](https://github.com/pagopa/io-app/commit/72719047270e00326d122519dc4f14de0bccf78b)) +* [[IOPID-1509](https://pagopa.atlassian.net/browse/IOPID-1509),[IOPID-1484](https://pagopa.atlassian.net/browse/IOPID-1484)] Integrate new DS on email insert/validation in onboarding flow and integrate new MP events ([#5632](https://github.com/pagopa/io-app/issues/5632)) ([2e0aeab](https://github.com/pagopa/io-app/commit/2e0aeab57ec1d7fe9bcc8ffd477f440ce8fe834c)) +* [[IOPID-1540](https://pagopa.atlassian.net/browse/IOPID-1540)] Adopt the new DS to `IdentificationModal` ([#5581](https://github.com/pagopa/io-app/issues/5581)) ([4d49bdb](https://github.com/pagopa/io-app/commit/4d49bdb114c1c82fc4733a97991cf5314b80a124)) +* [[IOPID-1566](https://pagopa.atlassian.net/browse/IOPID-1566),[IOPID-1567](https://pagopa.atlassian.net/browse/IOPID-1567),[IOPID-1570](https://pagopa.atlassian.net/browse/IOPID-1570)] Adopt the new DS to FL modals ([#5614](https://github.com/pagopa/io-app/issues/5614)) ([27146a9](https://github.com/pagopa/io-app/commit/27146a9bde2c467f1a8c8ff35fdbe0e6fc1913fd)) +* [[IOPLT-341](https://pagopa.atlassian.net/browse/IOPLT-341)] Rework e2e test job with matrix ([#5616](https://github.com/pagopa/io-app/issues/5616)) ([8892d5e](https://github.com/pagopa/io-app/commit/8892d5ec7a717889e83437c6ae92c315b1316bba)) +* [[IOPLT-359](https://pagopa.atlassian.net/browse/IOPLT-359)] Upgrade `react-native-svg` to 15.1.0 and `@pagopa/io-app-design-system` to 1.30.0 ([#5609](https://github.com/pagopa/io-app/issues/5609)) ([7d858bf](https://github.com/pagopa/io-app/commit/7d858bf260e3b7a09dbb05f8c7dec3e2bfb65b46)) +* [[IOPLT-384](https://pagopa.atlassian.net/browse/IOPLT-384)] Removes Mixpanel community library to use the official npm package ([#5601](https://github.com/pagopa/io-app/issues/5601)) ([93394d8](https://github.com/pagopa/io-app/commit/93394d803dfbb3ba5ad2be675b3801c669d1b2a2)), closes [/github.com/mixpanel/mixpanel-react-native/issues/89#issuecomment-904192090](https://github.com/pagopa//github.com/mixpanel/mixpanel-react-native/issues/89/issues/issuecomment-904192090) +* [[IOPLT-394](https://pagopa.atlassian.net/browse/IOPLT-394)] Bump Xcode version used to build the app ([#5611](https://github.com/pagopa/io-app/issues/5611)) ([9352f2c](https://github.com/pagopa/io-app/commit/9352f2c063081514d59d5142073ce9e037ca5ae0)) +* **Firma con IO:** [[SFEQS-2137](https://pagopa.atlassian.net/browse/SFEQS-2137)] Make PDF annotations read-only on iOS ([#5599](https://github.com/pagopa/io-app/issues/5599)) ([eb1ec7e](https://github.com/pagopa/io-app/commit/eb1ec7e37d9cb0b97d0021b332e8fb2275ff24fb)) + + +### Bug Fixes + +* [[IOBP-593](https://pagopa.atlassian.net/browse/IOBP-593)] Fix back button in PSP selection screen during payment ([#5634](https://github.com/pagopa/io-app/issues/5634)) ([1ad5a34](https://github.com/pagopa/io-app/commit/1ad5a3434ea4a723a7a036516bd293a7f83eedfa)) +* update minor version on cocoapods ([#5638](https://github.com/pagopa/io-app/issues/5638)) ([ff938f8](https://github.com/pagopa/io-app/commit/ff938f8db2a34587cc7b209911e9368803fa1b54)) +* **Cross:** [[IOAPPX-263](https://pagopa.atlassian.net/browse/IOAPPX-263)] IOS crash during root modal hiding ([#5600](https://github.com/pagopa/io-app/issues/5600)) ([56ffbf8](https://github.com/pagopa/io-app/commit/56ffbf82ab444e6a8e1902f397050e590ce908f7)) +* **Cross:** [[IOAPPX-266](https://pagopa.atlassian.net/browse/IOAPPX-266)] Fix crash coming from background due to Native Base ([af4be64](https://github.com/pagopa/io-app/commit/af4be645c06318b5804dd841b7d206e4b141ff44)) + + +### Chores + +* [[IOPAE-1045](https://pagopa.atlassian.net/browse/IOPAE-1045)] Add new service details base screen ([#5607](https://github.com/pagopa/io-app/issues/5607)) ([9eddc94](https://github.com/pagopa/io-app/commit/9eddc945e25083f3dab8734dc0b941d255e886b1)) +* **IT Wallet:** [[SIW-952](https://pagopa.atlassian.net/browse/SIW-952)] Add CGN card in new wallet section ([#5626](https://github.com/pagopa/io-app/issues/5626)) ([aa74914](https://github.com/pagopa/io-app/commit/aa74914df4dd7e7560ce9e96e3b945c2e7833ee3)) +* [[IOBP-606](https://pagopa.atlassian.net/browse/IOBP-606)] Add new payments section ([#5635](https://github.com/pagopa/io-app/issues/5635)) ([d670a68](https://github.com/pagopa/io-app/commit/d670a6898bffddef1db05b5d86aceec8777e67af)) +* **Cross:** [[IOAPPX-256](https://pagopa.atlassian.net/browse/IOAPPX-256)] Remove local ToastProvider ([#5557](https://github.com/pagopa/io-app/issues/5557)) ([49e2fc0](https://github.com/pagopa/io-app/commit/49e2fc021d5886268d7d7e878722a3789db68818)) +* **Cross:** [[IOAPPX-270](https://pagopa.atlassian.net/browse/IOAPPX-270)] Update `io-app-design-system` to `1.31.0` ([#5637](https://github.com/pagopa/io-app/issues/5637)) ([df52001](https://github.com/pagopa/io-app/commit/df52001d953791aabb8862375b124c892970d52b)) +* **IT Wallet:** [[SIW-950](https://pagopa.atlassian.net/browse/SIW-950)] Add ID Pay initiatives cards in new wallet section ([#5625](https://github.com/pagopa/io-app/issues/5625)) ([c0df60f](https://github.com/pagopa/io-app/commit/c0df60f943c54037fb1be5c0ff5b27947dbd1a52)) +* **IT Wallet:** [[SIW-954](https://pagopa.atlassian.net/browse/SIW-954)] Add new wallet base UI components ([#5619](https://github.com/pagopa/io-app/issues/5619)) ([dc01ce1](https://github.com/pagopa/io-app/commit/dc01ce162b3d7b28bd981bd7f5bd9e34ea8cbb10)) +* [[IOBP-562](https://pagopa.atlassian.net/browse/IOBP-562)] Full state layout and basics for new payments page ([#5535](https://github.com/pagopa/io-app/issues/5535)) ([e9aa9a5](https://github.com/pagopa/io-app/commit/e9aa9a5b353eccc8a5542eac8059f485bcaf8fda)) +* [[IOBP-568](https://pagopa.atlassian.net/browse/IOBP-568)] Add email to payment outcome 17 description ([#5615](https://github.com/pagopa/io-app/issues/5615)) ([2a3d57d](https://github.com/pagopa/io-app/commit/2a3d57d86c12c74ff6284f87cd9c16c895e836dc)) +* [[IOBP-578](https://pagopa.atlassian.net/browse/IOBP-578),[IOBP-577](https://pagopa.atlassian.net/browse/IOBP-577)] Add dynamic payment method logos ([#5612](https://github.com/pagopa/io-app/issues/5612)) ([dd59b52](https://github.com/pagopa/io-app/commit/dd59b5272ff35c666e42de33babb130f64914250)) +* [[IOBP-592](https://pagopa.atlassian.net/browse/IOBP-592)] Updated wallet and e-commerce definitions ([#5618](https://github.com/pagopa/io-app/issues/5618)) ([9a968a0](https://github.com/pagopa/io-app/commit/9a968a0ab6a74a695fe9d49ebf8da671e4062b4c)) +* [[IOBP-597](https://pagopa.atlassian.net/browse/IOBP-597)] Add new payment card component ([#5608](https://github.com/pagopa/io-app/issues/5608)) ([6bdf41d](https://github.com/pagopa/io-app/commit/6bdf41d78436c6b6a5b64b433513351ae8a6ba39)) +* [[PE-564](https://pagopa.atlassian.net/browse/PE-564)] CGN Activation KO pages ([#5629](https://github.com/pagopa/io-app/issues/5629)) ([de6a9f7](https://github.com/pagopa/io-app/commit/de6a9f75f04501a3b2e34d83d664d3a9fbf0a5a4)) +* **release:** 2.56.0-rc.4 ([9604c2d](https://github.com/pagopa/io-app/commit/9604c2d04d8cc37e7049870670e43b29727f831f)) +* [[IOPAE-1058](https://pagopa.atlassian.net/browse/IOPAE-1058)] Add services local feature flag ([#5605](https://github.com/pagopa/io-app/issues/5605)) ([18e480e](https://github.com/pagopa/io-app/commit/18e480eb5668ab4cb8d21534f3bad7e0fde46e74)) +* **IT Wallet:** [[SIW-957](https://pagopa.atlassian.net/browse/SIW-957)] Add wallet card component for IdPay initiatives ([#5610](https://github.com/pagopa/io-app/issues/5610)) ([13b1983](https://github.com/pagopa/io-app/commit/13b1983cc97659b6e6d0df3d344dfe0d3888a232)) + +## [2.56.0-rc.4](https://github.com/pagopa/io-app/compare/2.56.0-rc.3...2.56.0-rc.4) (2024-03-25) + + +### Chores + +* update patches.md ([9361cfb](https://github.com/pagopa/io-app/commit/9361cfbaa4e51ecac4911594d0be49cb7200e763)) + +## [2.56.0-rc.3](https://github.com/pagopa/io-app/compare/2.56.0-rc.2...2.56.0-rc.3) (2024-03-20) + + +### Features + +* [[IOPID-1511](https://pagopa.atlassian.net/browse/IOPID-1511)] [[IOPID-1529](https://pagopa.atlassian.net/browse/IOPID-1529)] Add new DS in Services Preference Screen (Profile and Onboarding) ([#5544](https://github.com/pagopa/io-app/issues/5544)) ([9e0452a](https://github.com/pagopa/io-app/commit/9e0452ae870925acf854c06c8b3216cb58a8fb63)) + +## [2.56.0-rc.2](https://github.com/pagopa/io-app/compare/2.56.0-rc.1...2.56.0-rc.2) (2024-03-20) + + +### Chores + +* bump io-app-design-system from 1.28.0 to 1.28.1 ([57dc0b5](https://github.com/pagopa/io-app/commit/57dc0b556a8e96d4eaefd5a6359a6af5df517859)) + +## [2.56.0-rc.1](https://github.com/pagopa/io-app/compare/2.56.0-rc.0...2.56.0-rc.1) (2024-03-19) + + +### Features + +* [[IOCOM-856](https://pagopa.atlassian.net/browse/IOCOM-856)] Add CTAs to `MessageDetailsScreen` ([#5582](https://github.com/pagopa/io-app/issues/5582)) ([e524884](https://github.com/pagopa/io-app/commit/e524884e5e403c78c94ab81909abbad1f6c91504)) +* [[IOCOM-859](https://pagopa.atlassian.net/browse/IOCOM-859)] New DS payment button on standard message details ([#5583](https://github.com/pagopa/io-app/issues/5583)) ([3ae055f](https://github.com/pagopa/io-app/commit/3ae055f2863316077829ba757c392af4ce8af007)) +* [[IOPLT-367](https://pagopa.atlassian.net/browse/IOPLT-367)] Adds the local dark mode toggle for development purpose ([#5576](https://github.com/pagopa/io-app/issues/5576)) ([af772e5](https://github.com/pagopa/io-app/commit/af772e53f23309f44d2beaacb56822d4d511ca51)) + + +### Bug Fixes + +* [[IABT-1540](https://pagopa.atlassian.net/browse/IABT-1540)] Fix expired session handling in payment flow ([#5568](https://github.com/pagopa/io-app/issues/5568)) ([c2be1bd](https://github.com/pagopa/io-app/commit/c2be1bd4398ec8dd5f668976b6be7eeed2037198)) +* [[IOBP-596](https://pagopa.atlassian.net/browse/IOBP-596)] Fix unexpected behavior in wallet onboarding Webview on Android devices ([#5594](https://github.com/pagopa/io-app/issues/5594)) ([4e47fda](https://github.com/pagopa/io-app/commit/4e47fdad8bb6a6b2a7525455e4e0b186c325a2f8)) +* theme experimental switch doesn't rerender ([#5591](https://github.com/pagopa/io-app/issues/5591)) ([9b8f9f7](https://github.com/pagopa/io-app/commit/9b8f9f7d3458fd05a076796821235d995d0deee8)) + + +### Chores + +* [[IOBP-589](https://pagopa.atlassian.net/browse/IOBP-589)] Adapted new payment flow with new swagger definitions ([#5593](https://github.com/pagopa/io-app/issues/5593)) ([c64e253](https://github.com/pagopa/io-app/commit/c64e2538363a5865f92427c6fbbf7ac241a4949b)) +* **Cross:** [[IOAPPX-261](https://pagopa.atlassian.net/browse/IOAPPX-261)] Add the new `contrast` variant to `ButtonLink` in the DS section ([#5590](https://github.com/pagopa/io-app/issues/5590)) ([5a6b185](https://github.com/pagopa/io-app/commit/5a6b18512855b466cd48b2166315435f3d401db0)) +* **IT Wallet:** [[SIW-907](https://pagopa.atlassian.net/browse/SIW-907)] Add StrongBox Mixpanel tracking for Lollipop keys ([#5580](https://github.com/pagopa/io-app/issues/5580)) ([892f473](https://github.com/pagopa/io-app/commit/892f473ca2819de9eaf8b1e6fac5313bf75836da)) +* **IT Wallet:** [[SIW-940](https://pagopa.atlassian.net/browse/SIW-940)] Add new wallet section screen with empty state ([#5588](https://github.com/pagopa/io-app/issues/5588)) ([ad7c9e8](https://github.com/pagopa/io-app/commit/ad7c9e8432569c8eeb07e9c877cb560e237bb475)) +* **IT Wallet:** [[SIW-941](https://pagopa.atlassian.net/browse/SIW-941)] Update Pr title with SIW id ([#5587](https://github.com/pagopa/io-app/issues/5587)) ([f4f60b3](https://github.com/pagopa/io-app/commit/f4f60b345c11bd2d9a87d559c1cf2540602cc6cc)) +* **IT Wallet:** [[SIW-942](https://pagopa.atlassian.net/browse/SIW-942)] Add payments section redirect banner within wallet section ([#5589](https://github.com/pagopa/io-app/issues/5589)) ([e9624d0](https://github.com/pagopa/io-app/commit/e9624d0768f6de8a716ea5ac203f96fb28365284)) +* **IT Wallet:** [[SIW-948](https://pagopa.atlassian.net/browse/SIW-948)] Add reducers and selectors for cards in the new wallet section ([#5604](https://github.com/pagopa/io-app/issues/5604)) ([1f561f4](https://github.com/pagopa/io-app/commit/1f561f44622337b7c3b1652c07793c8b617fdfe3)) +* [[SIW-939](https://pagopa.atlassian.net/browse/SIW-939)] Add new wallet section FF ([#5584](https://github.com/pagopa/io-app/issues/5584)) ([5ce9adb](https://github.com/pagopa/io-app/commit/5ce9adb04d83e04bbaa7db7ca341c42c3059935c)) + +## [2.56.0-rc.0](https://github.com/pagopa/io-app/compare/2.55.0-rc.3...2.56.0-rc.0) (2024-03-12) + + +### Features + +* [[IOCOM-854](https://pagopa.atlassian.net/browse/IOCOM-854)] Add bottom sheet to display message contacts ([#5570](https://github.com/pagopa/io-app/issues/5570)) ([063fe4d](https://github.com/pagopa/io-app/commit/063fe4d3bbd306f5dcb3872d58ed7c42e7f51e76)) +* [[IOCOM-857](https://pagopa.atlassian.net/browse/IOCOM-857)] Payment data on message details ([#5575](https://github.com/pagopa/io-app/issues/5575)) ([32da27d](https://github.com/pagopa/io-app/commit/32da27d5e0a0cc98c14fb6b1a5138abd0ac658fb)) +* [[IOPID-1499](https://pagopa.atlassian.net/browse/IOPID-1499)] Adopt the new DS layout to splash screens ([#5554](https://github.com/pagopa/io-app/issues/5554)) ([a8b32f4](https://github.com/pagopa/io-app/commit/a8b32f47efe1d6402a96c9313b77aef69b70197a)) +* [[IOPID-1504](https://pagopa.atlassian.net/browse/IOPID-1504)] Adopt the new DS to `IdpSuccessfulAuthentication` ([#5547](https://github.com/pagopa/io-app/issues/5547)) ([7357f9f](https://github.com/pagopa/io-app/commit/7357f9f55f88e80b6c8bcd257bc5d1093e9eaecd)) +* [[IOPID-1512](https://pagopa.atlassian.net/browse/IOPID-1512)] New DS on onboarding finished thank you page ([#5562](https://github.com/pagopa/io-app/issues/5562)) ([c73b6a4](https://github.com/pagopa/io-app/commit/c73b6a4a2764e27cd379999e8786fee0c21d951d)) +* [[IOPLT-342](https://pagopa.atlassian.net/browse/IOPLT-342)] Bump react-native version to 0.70 ([#5506](https://github.com/pagopa/io-app/issues/5506)) ([033eec2](https://github.com/pagopa/io-app/commit/033eec2e547f364d4b6ada6a1004cdbfb59e9bb5)) + + +### Bug Fixes + +* [[IOBP-513](https://pagopa.atlassian.net/browse/IOBP-513)] Added loading state in onboarding list wallet ([#5456](https://github.com/pagopa/io-app/issues/5456)) ([6f11e44](https://github.com/pagopa/io-app/commit/6f11e4447fa8478a8aaa317115d52c04d9e902f8)) +* [[IOBP-582](https://pagopa.atlassian.net/browse/IOBP-582)] Behavior of the new payment go back with only one PSP ([#5572](https://github.com/pagopa/io-app/issues/5572)) ([3687d7e](https://github.com/pagopa/io-app/commit/3687d7e443ec050fd73edb471da6c5149d0872d6)) +* e2e cache restore issue ([#5585](https://github.com/pagopa/io-app/issues/5585)) ([6050eee](https://github.com/pagopa/io-app/commit/6050eeeeb96a1dcb41ef96db887404abd1abd608)) + + +### Chores + +* [[IOBP-574](https://pagopa.atlassian.net/browse/IOBP-574)] Adds `success` flag to payment's Zendesk support payload ([#5546](https://github.com/pagopa/io-app/issues/5546)) ([93120f0](https://github.com/pagopa/io-app/commit/93120f0704c0349193c7f05f5a712d31a84d2568)) +* **Cross:** [[IOAPPX-249](https://pagopa.atlassian.net/browse/IOAPPX-249)] Update `build.gradle` ([#5512](https://github.com/pagopa/io-app/issues/5512)) ([51ce2d0](https://github.com/pagopa/io-app/commit/51ce2d06fdbcb50a7b6c61077ecf573e5fde0fc8)) +* **Cross:** [[IOAPPX-257](https://pagopa.atlassian.net/browse/IOAPPX-257)] Upload iOS dSYM files as artifact ([#5574](https://github.com/pagopa/io-app/issues/5574)) ([c51c295](https://github.com/pagopa/io-app/commit/c51c295bc0c7360f5271f9478e60402e182a86aa)) +* [[IOBP-581](https://pagopa.atlassian.net/browse/IOBP-581)] Changed text into payment cancelled by user outcome screen ([#5573](https://github.com/pagopa/io-app/issues/5573)) ([63e1746](https://github.com/pagopa/io-app/commit/63e1746f30be410c440886571d42b77b32ff3d7f)) + +## [2.55.0-rc.3](https://github.com/pagopa/io-app/compare/2.55.0-rc.2...2.55.0-rc.3) (2024-03-05) + + +### Features + +* [[IOCOM-853](https://pagopa.atlassian.net/browse/IOCOM-853)] Add footer component to MessageDetailsScreen ([#5560](https://github.com/pagopa/io-app/issues/5560)) ([b3ad3e4](https://github.com/pagopa/io-app/commit/b3ad3e427dd21d0842af6ee8253b5dbda9222a6d)) +* [[IOCOM-855](https://pagopa.atlassian.net/browse/IOCOM-855)] Add bottom sheet to display message extra data ([#5565](https://github.com/pagopa/io-app/issues/5565)) ([dc46b30](https://github.com/pagopa/io-app/commit/dc46b301590cb6ce953cb76af52be8bf7b205b65)) +* [[IOCOM-858](https://pagopa.atlassian.net/browse/IOCOM-858)] Generic payment update for messages ([#5567](https://github.com/pagopa/io-app/issues/5567)) ([8557640](https://github.com/pagopa/io-app/commit/8557640d7ad7d420db0ddd5d8f8e21cf1a552018)) +* [[IOPID-1500](https://pagopa.atlassian.net/browse/IOPID-1500)] Adopt the new DS to `IngressScreen` ([#5548](https://github.com/pagopa/io-app/issues/5548)) ([7350615](https://github.com/pagopa/io-app/commit/7350615dc6586efed94c52f17eec7038536a56db)) + + +### Bug Fixes + +* [[IOPID-1630](https://pagopa.atlassian.net/browse/IOPID-1630)] EIC error component navigation object not found ([#5571](https://github.com/pagopa/io-app/issues/5571)) ([8cb08e6](https://github.com/pagopa/io-app/commit/8cb08e6e253cff229c74bbf163573569749b5db2)) +* **Firma con IO:** [[SFEQS-2132](https://pagopa.atlassian.net/browse/SFEQS-2132)] Rerender when starting from page 0 in PDF preview ([#5569](https://github.com/pagopa/io-app/issues/5569)) ([b3c521f](https://github.com/pagopa/io-app/commit/b3c521fadf6df98d4492d1d490036c2afcbc0de2)) + + +### Chores + +* **Cross:** [[IOAPPX-259](https://pagopa.atlassian.net/browse/IOAPPX-259)] Upgrade `io-app-design-system` to `v.1.25.1` ([#5566](https://github.com/pagopa/io-app/issues/5566)) ([67f8ba0](https://github.com/pagopa/io-app/commit/67f8ba0a77fbe7ae4ee7b4405fc80694d587db27)) +* 🇩🇪 updates ([#5564](https://github.com/pagopa/io-app/issues/5564)) ([46ec138](https://github.com/pagopa/io-app/commit/46ec13805bcedcdbb37743ed64819f8fc847fb57)) +* **Cross:** [[IOAPPX-254](https://pagopa.atlassian.net/browse/IOAPPX-254)] Add iOS smooth corners to specific components ([#5552](https://github.com/pagopa/io-app/issues/5552)) ([7d5f362](https://github.com/pagopa/io-app/commit/7d5f362ddb4cb946b5f66cb2d2399e9bdd3a7c0a)) + +## [2.55.0-rc.2](https://github.com/pagopa/io-app/compare/2.55.0-rc.1...2.55.0-rc.2) (2024-03-01) + + +### Bug Fixes + +* [[IOCOM-1154](https://pagopa.atlassian.net/browse/IOCOM-1154)] Fix for `undefined` value on `isDesignSystemEnabled` (and double navigation bar) ([#5563](https://github.com/pagopa/io-app/issues/5563)) ([7d4414d](https://github.com/pagopa/io-app/commit/7d4414d6f745f7f5f68f5587a9dfc76e53542a92)) + + +### Chores + +* [[IOBP-561](https://pagopa.atlassian.net/browse/IOBP-561)] Addition of new payments page playground ([#5529](https://github.com/pagopa/io-app/issues/5529)) ([09b490a](https://github.com/pagopa/io-app/commit/09b490a888a4db071dc4477a6e71e97e97d30dba)) +* [[IOBP-569](https://pagopa.atlassian.net/browse/IOBP-569)] Removed expiry date from payment summary module checkout ([#5542](https://github.com/pagopa/io-app/issues/5542)) ([3355897](https://github.com/pagopa/io-app/commit/33558975ad36a8a75c1e166d8d21b9a76960dd28)) + +## [2.55.0-rc.1](https://github.com/pagopa/io-app/compare/2.55.0-rc.0...2.55.0-rc.1) (2024-03-01) + + +### Features + +* [[IOPID-1501](https://pagopa.atlassian.net/browse/IOPID-1501)] Adapt LandingScreen to the new DS ([#5536](https://github.com/pagopa/io-app/issues/5536)) ([9c4c4d1](https://github.com/pagopa/io-app/commit/9c4c4d14cedbe79a2d3738ffc9ce0ad73e6cb402)) + + +### Bug Fixes + +* [[IOCOM-1154](https://pagopa.atlassian.net/browse/IOCOM-1154)] Fix for double navigation bar on message details ([#5559](https://github.com/pagopa/io-app/issues/5559)) ([3bad813](https://github.com/pagopa/io-app/commit/3bad813f1aeafe8117c76de95bfa134a6a24ce4b)) + +## [2.55.0-rc.0](https://github.com/pagopa/io-app/compare/2.54.0-rc.1...2.55.0-rc.0) (2024-02-28) + + +### Features + +* [[IOCOM-1051](https://pagopa.atlassian.net/browse/IOCOM-1051)] Native HTTP client ([#5550](https://github.com/pagopa/io-app/issues/5550)) ([910a39c](https://github.com/pagopa/io-app/commit/910a39cc3de5a168cc0843e2e5b84078685b95fb)) +* **Firma con IO:** [[SFEQS-2083](https://pagopa.atlassian.net/browse/SFEQS-2083)] Replace abort bottom sheet with alert ([#5425](https://github.com/pagopa/io-app/issues/5425)) ([9d33b39](https://github.com/pagopa/io-app/commit/9d33b39b0db8c318a4047ef2879cbc4d441bfacf)) +* [[IOCOM-848](https://pagopa.atlassian.net/browse/IOCOM-848)] Message content with new DS System ([#5519](https://github.com/pagopa/io-app/issues/5519)) ([1560834](https://github.com/pagopa/io-app/commit/1560834b6bc3d964b252172a8988388e45edccda)) +* [[IOCOM-881](https://pagopa.atlassian.net/browse/IOCOM-881)] Update message attachment preview ([#5537](https://github.com/pagopa/io-app/issues/5537)) ([541e5c7](https://github.com/pagopa/io-app/commit/541e5c72357fc857d1ef52b2154bf95ecd7fad04)) + + +### Bug Fixes + +* [[IOCOM-1154](https://pagopa.atlassian.net/browse/IOCOM-1154)] Fix for double navigation bar on message details ([#5555](https://github.com/pagopa/io-app/issues/5555)) ([86ba3d3](https://github.com/pagopa/io-app/commit/86ba3d3a9424c1fe4a693db020de56b1b8ddda7f)) +* **Firma con IO:** [[SFEQS-2123](https://pagopa.atlassian.net/browse/SFEQS-2123)] Loading spinner not following DS toggle ([#5545](https://github.com/pagopa/io-app/issues/5545)) ([106c7df](https://github.com/pagopa/io-app/commit/106c7df8e95e2c9d7c7dd982f75d6103b0d4a768)) +* **Firma con IO:** [[SFEQS-2124](https://pagopa.atlassian.net/browse/SFEQS-2124)] Wrong margins in FCI flow ([#5543](https://github.com/pagopa/io-app/issues/5543)) ([dd16b6b](https://github.com/pagopa/io-app/commit/dd16b6b1756fa6d282c38279cc763ae49642e6cc)) +* [[IOBP-542](https://pagopa.atlassian.net/browse/IOBP-542)] StatusBar background color for Android devices in wallet details screen ([#5539](https://github.com/pagopa/io-app/issues/5539)) ([fe64210](https://github.com/pagopa/io-app/commit/fe642100928e85557293d29617d172d23f35d6bc)) + + +### Chores + +* [[IOBP-498](https://pagopa.atlassian.net/browse/IOBP-498)] Adds stepper in payment's screens header ([#5525](https://github.com/pagopa/io-app/issues/5525)) ([d09e9b0](https://github.com/pagopa/io-app/commit/d09e9b08b452bcef8526483d2956c05b014a4fe9)) +* [[IOBP-503](https://pagopa.atlassian.net/browse/IOBP-503)] Add payment attempts tracking ([#5401](https://github.com/pagopa/io-app/issues/5401)) ([54f967a](https://github.com/pagopa/io-app/commit/54f967a30ecf44bc60a43a0d33f1b5175a13182a)) +* [[IOBP-530](https://pagopa.atlassian.net/browse/IOBP-530)] Removed CTA button in payment confirmation screen with one PSP element ([#5488](https://github.com/pagopa/io-app/issues/5488)) ([69af3d9](https://github.com/pagopa/io-app/commit/69af3d923da3e2e5bdfea47c97eb3cc78eed2761)) +* [[IOBP-566](https://pagopa.atlassian.net/browse/IOBP-566)] Renames `walletV3` feature to `payments` ([#5533](https://github.com/pagopa/io-app/issues/5533)) ([4e32f86](https://github.com/pagopa/io-app/commit/4e32f8679863b0ef1d080ef57a9cb18af6c50374)) +* [[IOBP-572](https://pagopa.atlassian.net/browse/IOBP-572)] Move currency symbol to the right of the amount ([#5540](https://github.com/pagopa/io-app/issues/5540)) ([691b194](https://github.com/pagopa/io-app/commit/691b194855b0e8cb6071f22d65fcc9fa27a8e097)) +* [[IOPID-1481](https://pagopa.atlassian.net/browse/IOPID-1481)], [[IOPID-1482](https://pagopa.atlassian.net/browse/IOPID-1482)] Countdown 60 seconds ([#5514](https://github.com/pagopa/io-app/issues/5514)) ([2befb34](https://github.com/pagopa/io-app/commit/2befb345ab417d41a0a33c84c522331f4bdbb64a)) + +## [2.54.0-rc.1](https://github.com/pagopa/io-app/compare/2.54.0-rc.0...2.54.0-rc.1) (2024-02-21) + + +### Features + +* [[IOCOM-1059](https://pagopa.atlassian.net/browse/IOCOM-1059)] Remove 'Open' CTA on Android attachment preview (new DS) ([#5523](https://github.com/pagopa/io-app/issues/5523)) ([eb0691f](https://github.com/pagopa/io-app/commit/eb0691f0eef1ae3bf352fb2dc4f86362896f1ba2)) +* [[IOCOM-851](https://pagopa.atlassian.net/browse/IOCOM-851)] Add calendar event for messages with `due_date` ([#5524](https://github.com/pagopa/io-app/issues/5524)) ([12acdbe](https://github.com/pagopa/io-app/commit/12acdbe5a9c0309a63050f2464ae43d412d7785b)) +* [[IOCOM-851](https://pagopa.atlassian.net/browse/IOCOM-851)] Removed view reference ([#5511](https://github.com/pagopa/io-app/issues/5511)) ([39a2611](https://github.com/pagopa/io-app/commit/39a26119bf56ac5f517628888998dbe3be741504)) + + +### Bug Fixes + +* HeaderFirstLevel glitching on tab change ([#5530](https://github.com/pagopa/io-app/issues/5530)) ([d69e091](https://github.com/pagopa/io-app/commit/d69e091d888cca94c8ce9717659ff295412e559d)) +* **Cross:** [[IOAPPX-250](https://pagopa.atlassian.net/browse/IOAPPX-250)] Crash when consuming safe area insets outside of navigation context ([#5522](https://github.com/pagopa/io-app/issues/5522)) ([5ce18c2](https://github.com/pagopa/io-app/commit/5ce18c2d58c26eb00cc5d460474c7453c543c939)) + + +### Chores + +* **deps:** bump ip from 1.1.5 to 1.1.9 ([#5532](https://github.com/pagopa/io-app/issues/5532)) ([afc36a7](https://github.com/pagopa/io-app/commit/afc36a7742ac872f12a454b711d791509bcf01ee)) +* [[IOBP-555](https://pagopa.atlassian.net/browse/IOBP-555)] Add payment outcome screen for the outcome 17 and 15 ([#5527](https://github.com/pagopa/io-app/issues/5527)) ([755926d](https://github.com/pagopa/io-app/commit/755926d48cf101286847f934b59c3173f7f24b3b)) +* **Cross:** [[IOAPPX-252](https://pagopa.atlassian.net/browse/IOAPPX-252)] Update IO app icon in the main README ([#5531](https://github.com/pagopa/io-app/issues/5531)) ([f29ae25](https://github.com/pagopa/io-app/commit/f29ae2520e841554c13ee2d094749067d66a977d)) +* [[IOBP-544](https://pagopa.atlassian.net/browse/IOBP-544)] Add `usePagoPaPayment` custom hook ([#5503](https://github.com/pagopa/io-app/issues/5503)) ([b7257ad](https://github.com/pagopa/io-app/commit/b7257ad29a0c7ccf0d39a87e22c0a08559286c41)) +* [[IOBP-549](https://pagopa.atlassian.net/browse/IOBP-549)] Add new wallet payments history tracking ([#5515](https://github.com/pagopa/io-app/issues/5515)) ([ca54cd8](https://github.com/pagopa/io-app/commit/ca54cd8352b97bd4fe50b91c37c29c3987d43fb2)) +* [[IOBP-552](https://pagopa.atlassian.net/browse/IOBP-552),[IOBP-556](https://pagopa.atlassian.net/browse/IOBP-556)] Additional onboarding outcome screen for BPay ([#5518](https://github.com/pagopa/io-app/issues/5518)) ([de192e6](https://github.com/pagopa/io-app/commit/de192e6e4e48303bc9a037bdcf39d866c918c365)) +* [[IOBP-557](https://pagopa.atlassian.net/browse/IOBP-557)] Handled 404 response status with empty list from user wallet list ([#5528](https://github.com/pagopa/io-app/issues/5528)) ([a78bbce](https://github.com/pagopa/io-app/commit/a78bbce292b7987cdbe742e2c074a3fe15bf43d8)) +* [[IOBP-558](https://pagopa.atlassian.net/browse/IOBP-558)] Handle BancomatPay in method details screen ([#5517](https://github.com/pagopa/io-app/issues/5517)) ([505662b](https://github.com/pagopa/io-app/commit/505662b72745b377188360371c3aa7513a3975c1)) +* [[IOPLT-327](https://pagopa.atlassian.net/browse/IOPLT-327)] Updates Zendesk npm package version ([#5489](https://github.com/pagopa/io-app/issues/5489)) ([1dce8d5](https://github.com/pagopa/io-app/commit/1dce8d5329f34cdb0dd8dad375ddc21a8d413f5d)), closes [pagopa/io-react-native-zendesk#30](https://github.com/pagopa/io-react-native-zendesk/issues/30) +* **Cross:** [[IOAPPX-212](https://pagopa.atlassian.net/browse/IOAPPX-212)] Removes current route selector logic from header first level handler logic ([#5510](https://github.com/pagopa/io-app/issues/5510)) ([4400127](https://github.com/pagopa/io-app/commit/44001272b06441ba3644ba2c89ac8c083f2860c8)) +* **Cross:** [[IOAPPX-230](https://pagopa.atlassian.net/browse/IOAPPX-230)] Remove `datetimepicker` and `react-native-modal-datetime-picker` dependencies ([#5451](https://github.com/pagopa/io-app/issues/5451)) ([76c841d](https://github.com/pagopa/io-app/commit/76c841d7b9b574ea1dfe99c4bb166be313bf4582)) + +## [2.54.0-rc.0](https://github.com/pagopa/io-app/compare/2.53.0-rc.1...2.54.0-rc.0) (2024-02-15) + + +### Features + +* [[IOCOM-1011](https://pagopa.atlassian.net/browse/IOCOM-1011)] Accessibility on "Includes Attachments"-Icon on the new DS Message Details screen ([#5497](https://github.com/pagopa/io-app/issues/5497)) ([416aff7](https://github.com/pagopa/io-app/commit/416aff73e508ad2bf9950f20c30cafdca3eb4c25)) +* [[IOCOM-1012](https://pagopa.atlassian.net/browse/IOCOM-1012)] Better accessibility on PDF preview with the new DS system ([#5494](https://github.com/pagopa/io-app/issues/5494)) ([7689647](https://github.com/pagopa/io-app/commit/76896474e9d982178af94e06fa1351ff6b8b5173)) +* [[IOCOM-1029](https://pagopa.atlassian.net/browse/IOCOM-1029)] Add support for multi image in the detail of a message ([#5508](https://github.com/pagopa/io-app/issues/5508)) ([69c9be1](https://github.com/pagopa/io-app/commit/69c9be10f76ac84ff4447dd9a4c03461231d3405)) +* [[IOCOM-849](https://pagopa.atlassian.net/browse/IOCOM-849),[IOCOM-850](https://pagopa.atlassian.net/browse/IOCOM-850)] Add `MessageDetailsComponent` in the detail of a message ([#5495](https://github.com/pagopa/io-app/issues/5495)) ([2501d29](https://github.com/pagopa/io-app/commit/2501d2953cd2bfe798af69c98763279113a302fa)) +* [[IOCOM-851](https://pagopa.atlassian.net/browse/IOCOM-851)] Add `Alert` in the detail of a message that contains an expiring payment ([#5507](https://github.com/pagopa/io-app/issues/5507)) ([1e87608](https://github.com/pagopa/io-app/commit/1e876081caa635aebf6eb2db121ad3963153e86e)) +* [[IOCOM-862](https://pagopa.atlassian.net/browse/IOCOM-862),[IOCOM-863](https://pagopa.atlassian.net/browse/IOCOM-863)] New DS on the message's attachments list ([#5485](https://github.com/pagopa/io-app/issues/5485)) ([f06e592](https://github.com/pagopa/io-app/commit/f06e5921ca8652227b82da640a8140b29712331e)) + + +### Bug Fixes + +* [[IABT-1530](https://pagopa.atlassian.net/browse/IABT-1530)] Enabled header shown property on Transaction details screens ([#5493](https://github.com/pagopa/io-app/issues/5493)) ([333a40e](https://github.com/pagopa/io-app/commit/333a40e845440b27a7bbf38843b8743409d89082)) +* [[IOBP-460](https://pagopa.atlassian.net/browse/IOBP-460),[IOBP-461](https://pagopa.atlassian.net/browse/IOBP-461),[IOBP-463](https://pagopa.atlassian.net/browse/IOBP-463)] A11y for list item info components into transaction detail screen ([#5478](https://github.com/pagopa/io-app/issues/5478)) ([a49be25](https://github.com/pagopa/io-app/commit/a49be25c9a5766e59067801830a89a2b1095d320)) +* [[IOCOM-1091](https://pagopa.atlassian.net/browse/IOCOM-1091)] Fix for duplicate message in messages list ([#5509](https://github.com/pagopa/io-app/issues/5509)) ([cb14f8a](https://github.com/pagopa/io-app/commit/cb14f8ad0191d9ae66356bda5ef1dc67e39b5602)) +* [[IOPID-1423](https://pagopa.atlassian.net/browse/IOPID-1423),[IOPID-1439](https://pagopa.atlassian.net/browse/IOPID-1439)] Fix double MP event tracking ([#5496](https://github.com/pagopa/io-app/issues/5496)) ([fbd8cb3](https://github.com/pagopa/io-app/commit/fbd8cb3949d4f330949a65ad3d80e35044ef4129)) +* [[IOPID-1546](https://pagopa.atlassian.net/browse/IOPID-1546)] Fix services refresh token error ([#5505](https://github.com/pagopa/io-app/issues/5505)) ([366f208](https://github.com/pagopa/io-app/commit/366f2088b1f73eea784dd1f2ef0720088d054191)) + + +### Chores + +* **Cross:** [[IOAPPX-234](https://pagopa.atlassian.net/browse/IOAPPX-234)] Remove `@react-native-picker/picker` dependency ([#5454](https://github.com/pagopa/io-app/issues/5454)) ([3e98819](https://github.com/pagopa/io-app/commit/3e9881974908a2932320e45260b517b8827c8101)) +* **Cross:** [[IOAPPX-241](https://pagopa.atlassian.net/browse/IOAPPX-241)] Replace legacy `FooterWithButtons` in the `DownloadProfileData` screen ([#5472](https://github.com/pagopa/io-app/issues/5472)) ([027d442](https://github.com/pagopa/io-app/commit/027d442e945d7b9784cdeb5cbfc00df9b91f39b5)) +* **Cross:** [[IOAPPX-242](https://pagopa.atlassian.net/browse/IOAPPX-242)] Replace legacy `FooterWithButtons` in the 'Remove account' flow ([#5470](https://github.com/pagopa/io-app/issues/5470)) ([6cd3b34](https://github.com/pagopa/io-app/commit/6cd3b348064d277ed1fb5138ae8d45521ce84ec5)) +* **Cross:** [[IOAPPX-243](https://pagopa.atlassian.net/browse/IOAPPX-243)] Remove `native-base` components from private screens + Add new header managed by `react-navigation` ([#5482](https://github.com/pagopa/io-app/issues/5482)) ([912604e](https://github.com/pagopa/io-app/commit/912604e5f2723c21f15cd28a3a634e45dc3d5da6)) +* **Cross:** [[IOAPPX-245](https://pagopa.atlassian.net/browse/IOAPPX-245)] Remove `hound.yml` ([#5500](https://github.com/pagopa/io-app/issues/5500)) ([96ac530](https://github.com/pagopa/io-app/commit/96ac5307c65e8d3099b8284329b997abe04691f5)) +* **Cross:** [[IOAPPX-247](https://pagopa.atlassian.net/browse/IOAPPX-247)] Remove `_editorconfig` ([#5501](https://github.com/pagopa/io-app/issues/5501)) ([932b8bf](https://github.com/pagopa/io-app/commit/932b8bfbfd2e4893466f54dd6012f0ca6af7ff92)) +* [[IOBP-525](https://pagopa.atlassian.net/browse/IOBP-525)] Fix payment authorization webview on Android devices ([#5474](https://github.com/pagopa/io-app/issues/5474)) ([d67f85f](https://github.com/pagopa/io-app/commit/d67f85f14bd5cb415e1779c91899aeb56b64031d)) +* [[IOBP-533](https://pagopa.atlassian.net/browse/IOBP-533)] Navigation to payment confirm screen if method with only one PSP ([#5487](https://github.com/pagopa/io-app/issues/5487)) ([7e7c2c9](https://github.com/pagopa/io-app/commit/7e7c2c9336ecd47eba953f9f0d2999063895496c)) +* [[IOBP-535](https://pagopa.atlassian.net/browse/IOBP-535)] Added german language for iOS app ([#5491](https://github.com/pagopa/io-app/issues/5491)) ([ae5ffeb](https://github.com/pagopa/io-app/commit/ae5ffeb80012092caf87890c5b0a98a01593f3d2)) +* [[IOBP-546](https://pagopa.atlassian.net/browse/IOBP-546)] Fix payment E2E tests ([#5498](https://github.com/pagopa/io-app/issues/5498)) ([eae6e83](https://github.com/pagopa/io-app/commit/eae6e833dd5c7201f4ee061d87ee67220c53fcd4)) +* [[IOPID-1416](https://pagopa.atlassian.net/browse/IOPID-1416)] Fix logic to show toast error ([#5502](https://github.com/pagopa/io-app/issues/5502)) ([50ad939](https://github.com/pagopa/io-app/commit/50ad93995196c4228b1691748fb99dcc0c79548f)) +* [[IOPID-1448](https://pagopa.atlassian.net/browse/IOPID-1448)] Fix double profile upsert ([#5486](https://github.com/pagopa/io-app/issues/5486)) ([0d72c2a](https://github.com/pagopa/io-app/commit/0d72c2af4330af9a6ea53cd5984b80356766c6df)), closes [/github.com/pagopa/io-app/pull/5486/files#diff-a48057473d6b62f0ea5bfaf0ed2ba3e8a390fdef1e1e3ff7e9760fd8c4a86ff6](https://github.com/pagopa//github.com/pagopa/io-app/pull/5486/files/issues/diff-a48057473d6b62f0ea5bfaf0ed2ba3e8a390fdef1e1e3ff7e9760fd8c4a86ff6) [/github.com/pagopa/io-app/pull/5486/files#diff-8a5b2f3967d681b976fe673762bd1061f5b430130c880c1195b76af06362cf31](https://github.com/pagopa//github.com/pagopa/io-app/pull/5486/files/issues/diff-8a5b2f3967d681b976fe673762bd1061f5b430130c880c1195b76af06362cf31) +* **Cross:** [[IOAPPX-218](https://pagopa.atlassian.net/browse/IOAPPX-218)] Add the new `Titillium Sans Pro` typeface to the codebase ([#5459](https://github.com/pagopa/io-app/issues/5459)) ([20e54f4](https://github.com/pagopa/io-app/commit/20e54f4925c5bd460e49f6662d0f58d792d0317d)) +* **Cross:** [[IOAPPX-246](https://pagopa.atlassian.net/browse/IOAPPX-246)] Update `Podfile.lock` for `io-react-native-login-utils` ([#5499](https://github.com/pagopa/io-app/issues/5499)) ([e7ed331](https://github.com/pagopa/io-app/commit/e7ed3310efecc62b68b2d2b77808ffee42996496)) + +## [2.53.0-rc.1](https://github.com/pagopa/io-app/compare/2.53.0-rc.0...2.53.0-rc.1) (2024-02-06) + + +### Features + +* **Firma con IO:** [[SFEQS-2071](https://pagopa.atlassian.net/browse/SFEQS-2071)] Update `Firma con IO` with new DS components ([#5377](https://github.com/pagopa/io-app/issues/5377)) ([e34d79d](https://github.com/pagopa/io-app/commit/e34d79db5afdb45c5c91247c70afc54f911534aa)) +* [[IOCOM-867](https://pagopa.atlassian.net/browse/IOCOM-867)] Add service info in MessageDetailsScreen ([#5461](https://github.com/pagopa/io-app/issues/5461)) ([b2ea541](https://github.com/pagopa/io-app/commit/b2ea541609ff95afef6be15bb09cbffa682860fa)) + + +### Bug Fixes + +* [[IOBP-458](https://pagopa.atlassian.net/browse/IOBP-458),[IOBP-459](https://pagopa.atlassian.net/browse/IOBP-459)] Added accessibility label to the RNavScreenWithLargeHeader component ([#5467](https://github.com/pagopa/io-app/issues/5467)) ([a57d5e2](https://github.com/pagopa/io-app/commit/a57d5e2342be768bea5f3d5a92ae254dbd3e3725)) +* [[IOPID-1436](https://pagopa.atlassian.net/browse/IOPID-1436)], [[IOPID-1453](https://pagopa.atlassian.net/browse/IOPID-1453)] Delete helper button and header title on email validate screen and add logic to show support buttons in faq screen ([#5484](https://github.com/pagopa/io-app/issues/5484)) ([00743fd](https://github.com/pagopa/io-app/commit/00743fd5708ce4af62039ca7f6422bd22dbd747e)), closes [/github.com/pagopa/io-app/blob/6c076a1fee25c0a0853644d63aa894ff26854aa3/ts/sagas/startup.ts#L492](https://github.com/pagopa//github.com/pagopa/io-app/blob/6c076a1fee25c0a0853644d63aa894ff26854aa3/ts/sagas/startup.ts/issues/L492) +* align podfile.lock io-react-native-zendesk version ([#5483](https://github.com/pagopa/io-app/issues/5483)) ([6c076a1](https://github.com/pagopa/io-app/commit/6c076a1fee25c0a0853644d63aa894ff26854aa3)) + + +### Chores + +* **Cross:** [[IOAPPX-210](https://pagopa.atlassian.net/browse/IOAPPX-210)] Upgrade `react-navigation` to v6 ([#5415](https://github.com/pagopa/io-app/issues/5415)) ([f730e55](https://github.com/pagopa/io-app/commit/f730e55d493df6230319494fad43626bf2070a98)) +* [[IOBP-529](https://pagopa.atlassian.net/browse/IOBP-529)] Retrieve payments methods list from eCommerce client endpoint ([#5479](https://github.com/pagopa/io-app/issues/5479)) ([63fd1c2](https://github.com/pagopa/io-app/commit/63fd1c2a73bd3408996cd35d02e145e9e195579a)) +* 🇩🇪 language adjustments ([#5490](https://github.com/pagopa/io-app/issues/5490)) ([957e245](https://github.com/pagopa/io-app/commit/957e2455330caae4af29f3a33bda6b7360ffb1d2)) + ## [2.53.0-rc.0](https://github.com/pagopa/io-app/compare/2.52.0-rc.0...2.53.0-rc.0) (2024-02-02) diff --git a/Gemfile b/Gemfile index 5afbf1ed051..956c32566e2 100644 --- a/Gemfile +++ b/Gemfile @@ -4,5 +4,6 @@ source "https://rubygems.org" # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby '>=2.6.10' -gem "cocoapods", ">= 1.12.1" +gem "cocoapods", ">= 1.15.2" gem "fastlane", "~> 2.212.2" +gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 83c42a47175..0079aaf3314 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,16 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.0.7.2) + activesupport (7.0.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.1) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) @@ -32,13 +34,14 @@ GEM aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) - cocoapods (1.12.1) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.1) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -50,8 +53,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.1) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -62,7 +65,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -74,7 +77,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) @@ -153,7 +156,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - ffi (1.15.5) + ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) @@ -198,14 +201,14 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) jwt (2.7.1) mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.19.0) + minitest (5.22.3) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) @@ -213,6 +216,7 @@ GEM nap (1.1.0) naturally (2.2.1) netrc (0.11.0) + nkf (0.2.0) optparse (0.1.1) os (1.1.4) plist (3.7.1) @@ -223,7 +227,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -245,14 +249,14 @@ GEM tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) unicode-display_width (1.8.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -268,7 +272,8 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (>= 1.12.1) + activesupport (>= 6.1.7.3, < 7.1.0) + cocoapods (>= 1.15.2) fastlane (~> 2.212.2) RUBY VERSION diff --git a/README.md b/README.md index 8c1c79dec8c..27ba14798fd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-
+

IO - The public services app

diff --git a/_editorconfig b/_editorconfig deleted file mode 100644 index 7c286132fe9..00000000000 --- a/_editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -# Windows files -[*.bat] -end_of_line = crlf diff --git a/android/app/build.gradle b/android/app/build.gradle index d8c0b7bb2ed..c57ea58be7f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,6 +7,7 @@ apply plugin: "com.android.application" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" import com.android.build.OutputFile +import org.apache.tools.ant.taskdefs.condition.Os /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets @@ -144,26 +145,18 @@ android { applicationId "it.pagopa.io.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 100154794 - versionName "2.53.0.0" + versionCode 100154809 + versionName "2.58.0.0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { - // We configure the NDK build only if you decide to opt-in for the New Architecture. + // We configure the CMake build only if you decide to opt-in for the New Architecture. externalNativeBuild { - ndkBuild { - arguments "APP_PLATFORM=android-21", - "APP_STL=c++_shared", - "NDK_TOOLCHAIN_VERSION=clang", - "GENERATED_SRC_DIR=$buildDir/generated/source", - "PROJECT_BUILD_DIR=$buildDir", - "REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid", - "REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build", - "NODE_MODULES_DIR=$rootDir/../node_modules" - cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1" - cppFlags "-std=c++17" - // Make sure this target name is the same you specify inside the - // src/main/jni/Android.mk file for the `LOCAL_MODULE` variable. - targets "ioapp_appmodules" + cmake { + arguments "-DPROJECT_BUILD_DIR=$buildDir", + "-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid", + "-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build", + "-DNODE_MODULES_DIR=$rootDir/../node_modules", + "-DANDROID_STL=c++_shared" } } if (!enableSeparateBuildPerCPUArchitecture) { @@ -179,8 +172,8 @@ android { if (isNewArchitectureEnabled()) { // We configure the NDK build only if you decide to opt-in for the New Architecture. externalNativeBuild { - ndkBuild { - path "$projectDir/src/main/jni/Android.mk" + cmake { + path "$projectDir/src/main/jni/CMakeLists.txt" } } def reactAndroidProjectDir = project(':ReactAndroid').projectDir @@ -201,15 +194,15 @@ android { preDebugBuild.dependsOn(packageReactNdkDebugLibs) preReleaseBuild.dependsOn(packageReactNdkReleaseLibs) // Due to a bug inside AGP, we have to explicitly set a dependency - // between configureNdkBuild* tasks and the preBuild tasks. + // between configureCMakeDebug* tasks and the preBuild tasks. // This can be removed once this is solved: https://issuetracker.google.com/issues/207403732 - configureNdkBuildRelease.dependsOn(preReleaseBuild) - configureNdkBuildDebug.dependsOn(preDebugBuild) + configureCMakeRelWithDebInfo.dependsOn(preReleaseBuild) + configureCMakeDebug.dependsOn(preDebugBuild) reactNativeArchitectures().each { architecture -> - tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure { + tasks.findByName("configureCMakeDebug[${architecture}]")?.configure { dependsOn("preDebugBuild") } - tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure { + tasks.findByName("configureCMakeRelWithDebInfo[${architecture}]")?.configure { dependsOn("preReleaseBuild") } } @@ -334,13 +327,13 @@ dependencies { } } } - implementation project(':@react-native-community_datetimepicker') implementation project(':react-native-fingerprint-scanner') implementation project(':react-native-art') implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.41" implementation('com.google.firebase:firebase-iid:21.1.0') { because "Firebase messaging 22.0.0 removes Firebase Instance ID API but out current version of the mixpanel sdk requires it https://github.com/mixpanel/mixpanel-android/issues/744 https://firebase.google.com/support/release-notes/android#messaging_v22-0-0" } + implementation "androidx.constraintlayout:constraintlayout:2.1.4" } // Run this once to be able to run the application with BUCK diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Black.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Black.otf new file mode 100644 index 00000000000..3cd95e37eaf Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Black.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-BlackItalic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-BlackItalic.otf new file mode 100644 index 00000000000..a6fd8bc8af7 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-BlackItalic.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Bold.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Bold.otf new file mode 100644 index 00000000000..3d774ef1a03 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Bold.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-BoldItalic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-BoldItalic.otf new file mode 100644 index 00000000000..bdf1aaedfc9 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-BoldItalic.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Italic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Italic.otf new file mode 100644 index 00000000000..51f58e72f82 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Italic.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Light.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Light.otf new file mode 100644 index 00000000000..c75c48c9763 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Light.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-LightItalic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-LightItalic.otf new file mode 100644 index 00000000000..3146a822c96 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-LightItalic.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Regular.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Regular.otf new file mode 100644 index 00000000000..4bd7bcf2d5a Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Regular.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Semibold.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Semibold.otf new file mode 100644 index 00000000000..fc7186dda82 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Semibold.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-SemiboldItalic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-SemiboldItalic.otf new file mode 100644 index 00000000000..8242da46cf5 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-SemiboldItalic.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-Thin.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-Thin.otf new file mode 100644 index 00000000000..e9f73560c42 Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-Thin.otf differ diff --git a/android/app/src/main/assets/fonts/TitilliumSansPro-ThinItalic.otf b/android/app/src/main/assets/fonts/TitilliumSansPro-ThinItalic.otf new file mode 100644 index 00000000000..869febf3eea Binary files /dev/null and b/android/app/src/main/assets/fonts/TitilliumSansPro-ThinItalic.otf differ diff --git a/android/app/src/main/jni/Android.mk b/android/app/src/main/jni/Android.mk deleted file mode 100644 index ebb715535f1..00000000000 --- a/android/app/src/main/jni/Android.mk +++ /dev/null @@ -1,39 +0,0 @@ -THIS_DIR := $(call my-dir) -include $(REACT_ANDROID_DIR)/Android-prebuilt.mk -# If you wish to add a custom TurboModule or Fabric component in your app you -# will have to include the following autogenerated makefile. -# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk -include $(CLEAR_VARS) -LOCAL_PATH := $(THIS_DIR) -# You can customize the name of your application .so file here. -LOCAL_MODULE := ioapp_appmodules -LOCAL_C_INCLUDES := $(LOCAL_PATH) -LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) -LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) -# If you wish to add a custom TurboModule or Fabric component in your app you -# will have to uncomment those lines to include the generated source -# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni) -# -# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni -# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp) -# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni -# Here you should add any native library you wish to depend on. -LOCAL_SHARED_LIBRARIES := \ - libfabricjni \ - libfbjni \ - libfolly_runtime \ - libglog \ - libjsi \ - libreact_codegen_rncore \ - libreact_debug \ - libreact_nativemodule_core \ - libreact_render_componentregistry \ - libreact_render_core \ - libreact_render_debug \ - libreact_render_graphics \ - librrc_view \ - libruntimeexecutor \ - libturbomodulejsijni \ - libyoga -LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall -include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/android/app/src/main/jni/CMakeLists.txt b/android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 00000000000..0dd7f25118c --- /dev/null +++ b/android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.13) + +# Define the library name here. +project(ioapp_appmodules) + +# This file includes all the necessary to let you build your application with the New Architecture. +include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) diff --git a/android/app/src/main/jni/MainApplicationModuleProvider.cpp b/android/app/src/main/jni/MainApplicationModuleProvider.cpp index 640a5baac65..1e04d15b34f 100644 --- a/android/app/src/main/jni/MainApplicationModuleProvider.cpp +++ b/android/app/src/main/jni/MainApplicationModuleProvider.cpp @@ -1,9 +1,10 @@ #include "MainApplicationModuleProvider.h" +#include #include namespace facebook { namespace react { std::shared_ptr MainApplicationModuleProvider( - const std::string moduleName, + const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { // Here you can provide your own module provider for TurboModules coming from // either your application or from external libraries. The approach to follow @@ -14,6 +15,12 @@ std::shared_ptr MainApplicationModuleProvider( // return module; // } // return rncore_ModuleProvider(moduleName, params); + // Module providers autolinked by RN CLI + auto rncli_module = rncli_ModuleProvider(moduleName, params); + if (rncli_module != nullptr) { + return rncli_module; + } + return rncore_ModuleProvider(moduleName, params); } } // namespace react diff --git a/android/app/src/main/jni/MainApplicationModuleProvider.h b/android/app/src/main/jni/MainApplicationModuleProvider.h index f89ddbd02d5..4515c40bb83 100644 --- a/android/app/src/main/jni/MainApplicationModuleProvider.h +++ b/android/app/src/main/jni/MainApplicationModuleProvider.h @@ -5,7 +5,7 @@ namespace facebook { namespace react { std::shared_ptr MainApplicationModuleProvider( - const std::string moduleName, + const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); } // namespace react } // namespace facebook \ No newline at end of file diff --git a/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp b/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp index decb4e17e00..8439888b532 100644 --- a/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp +++ b/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp @@ -18,19 +18,19 @@ void MainApplicationTurboModuleManagerDelegate::registerNatives() { } std::shared_ptr MainApplicationTurboModuleManagerDelegate::getTurboModule( - const std::string name, - const std::shared_ptr jsInvoker) { + const std::string &name, + const std::shared_ptr &jsInvoker) { // Not implemented yet: provide pure-C++ NativeModules here. return nullptr; } std::shared_ptr MainApplicationTurboModuleManagerDelegate::getTurboModule( - const std::string name, + const std::string &name, const JavaTurboModule::InitParams ¶ms) { return MainApplicationModuleProvider(name, params); } bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( - std::string name) { + const std::string n&ame) { return getTurboModule(name, nullptr) != nullptr || getTurboModule(name, {.moduleName = name}) != nullptr; } diff --git a/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h b/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h index 3dc56868f80..c31481973d3 100644 --- a/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h +++ b/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h @@ -15,16 +15,16 @@ class MainApplicationTurboModuleManagerDelegate static jni::local_ref initHybrid(jni::alias_ref); static void registerNatives(); std::shared_ptr getTurboModule( - const std::string name, - const std::shared_ptr jsInvoker) override; + const std::string &name, + const std::shared_ptr &jsInvoker) override; std::shared_ptr getTurboModule( - const std::string name, + const std::string &name, const JavaTurboModule::InitParams ¶ms) override; /** * Test-only method. Allows user to verify whether a TurboModule can be * created by instances of this class. */ - bool canCreateTurboModule(std::string name); + bool canCreateTurboModule(const std::string &name); }; } // namespace react } // namespace facebook diff --git a/android/app/src/main/jni/MainComponentsRegistry.cpp b/android/app/src/main/jni/MainComponentsRegistry.cpp index 01da5875a8e..c634fa5bb2e 100644 --- a/android/app/src/main/jni/MainComponentsRegistry.cpp +++ b/android/app/src/main/jni/MainComponentsRegistry.cpp @@ -3,12 +3,17 @@ #include #include #include +#include + namespace facebook { namespace react { MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} std::shared_ptr MainComponentsRegistry::sharedProviderRegistry() { auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); + + // Autolinked providers registered by RN CLI + rncli_registerProviders(providerRegistry); // Custom Fabric Components go here. You can register custom // components coming from your App or from 3rd party libraries here. // diff --git a/android/app/src/main/res/drawable/eu_next_logo.xml b/android/app/src/main/res/drawable/eu_next_logo.xml new file mode 100644 index 00000000000..897c986a5a1 --- /dev/null +++ b/android/app/src/main/res/drawable/eu_next_logo.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/logo_io.xml b/android/app/src/main/res/drawable/logo_io.xml new file mode 100644 index 00000000000..e6243fb2683 --- /dev/null +++ b/android/app/src/main/res/drawable/logo_io.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/logo_pagopa.xml b/android/app/src/main/res/drawable/logo_pagopa.xml new file mode 100644 index 00000000000..4e44899d259 --- /dev/null +++ b/android/app/src/main/res/drawable/logo_pagopa.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml index 823ddd4beeb..783da7e2ec8 100644 --- a/android/app/src/main/res/layout/launch_screen.xml +++ b/android/app/src/main/res/layout/launch_screen.xml @@ -1,103 +1,66 @@ - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="30dp" + android:layout_marginTop="16dp" + android:gravity="center" + android:text="@string/splash_io_name" + android:textColor="#FFFFFF" + android:textSize="16sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/ioLogo" /> + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/eu_next_logo.png b/android/app/src/main/res/mipmap-hdpi/eu_next_logo.png deleted file mode 100644 index a37f053a9ee..00000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/eu_next_logo.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/logo_io.png b/android/app/src/main/res/mipmap-hdpi/logo_io.png deleted file mode 100644 index 2734d0b64d5..00000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/logo_io.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/logo_pagopa.png b/android/app/src/main/res/mipmap-hdpi/logo_pagopa.png deleted file mode 100644 index 65fcac8645d..00000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/logo_pagopa.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/eu_next_logo.png b/android/app/src/main/res/mipmap-mdpi/eu_next_logo.png deleted file mode 100644 index 99cc7c5710a..00000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/eu_next_logo.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/logo_io.png b/android/app/src/main/res/mipmap-mdpi/logo_io.png deleted file mode 100644 index c9364152e28..00000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/logo_io.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/logo_pagopa.png b/android/app/src/main/res/mipmap-mdpi/logo_pagopa.png deleted file mode 100644 index 15ffa878a1b..00000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/logo_pagopa.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/eu_next_logo.png b/android/app/src/main/res/mipmap-xhdpi/eu_next_logo.png deleted file mode 100644 index 4c19d91e38e..00000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/eu_next_logo.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/logo_io.png b/android/app/src/main/res/mipmap-xhdpi/logo_io.png deleted file mode 100644 index c9364152e28..00000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/logo_io.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/logo_pagopa.png b/android/app/src/main/res/mipmap-xhdpi/logo_pagopa.png deleted file mode 100644 index 15ffa878a1b..00000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/logo_pagopa.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/eu_next_logo.png b/android/app/src/main/res/mipmap-xxhdpi/eu_next_logo.png deleted file mode 100644 index 3d33a008152..00000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/eu_next_logo.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/logo_io.png b/android/app/src/main/res/mipmap-xxhdpi/logo_io.png deleted file mode 100644 index 333146f2cc5..00000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/logo_io.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/logo_pagopa.png b/android/app/src/main/res/mipmap-xxhdpi/logo_pagopa.png deleted file mode 100644 index 26ae73dd950..00000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/logo_pagopa.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/eu_next_logo.png b/android/app/src/main/res/mipmap-xxxhdpi/eu_next_logo.png deleted file mode 100644 index e915ebfd602..00000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/eu_next_logo.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/logo_io.png b/android/app/src/main/res/mipmap-xxxhdpi/logo_io.png deleted file mode 100644 index 333146f2cc5..00000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/logo_io.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/logo_pagopa.png b/android/app/src/main/res/mipmap-xxxhdpi/logo_pagopa.png deleted file mode 100644 index 26ae73dd950..00000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/logo_pagopa.png and /dev/null differ diff --git a/android/build.gradle b/android/build.gradle index 4c47f1fa60f..200c0cdea90 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -23,7 +23,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.facebook.react:react-native-gradle-plugin' classpath 'de.undercouch:gradle-download-task:5.0.1' classpath 'com.google.gms:google-services:4.3.3' @@ -36,21 +36,6 @@ buildscript { allprojects { repositories { - exclusiveContent { - // We get React Native's Android binaries exclusively through npm, - // from a local Maven repo inside node_modules/react-native/. - // (The use of exclusiveContent prevents looking elsewhere like Maven Central - // and potentially getting a wrong version.) - filter { - includeGroup "com.facebook.react" - } - forRepository { - maven { - url "$rootDir/../node_modules/react-native/android" - } - } - } - mavenLocal() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url("$rootDir/../node_modules/react-native/android") @@ -68,10 +53,7 @@ allprojects { } google() jcenter() - maven { url 'https://www.jitpack.io' } + maven { url 'https://www.jitpack.io' } maven { url 'https://zendesk.jfrog.io/zendesk/repo' } - } - configurations.all { - resolutionStrategy.force "com.android.support:support-v4:${rootProject.ext.supportLibVersion}" } } diff --git a/android/gradle.properties b/android/gradle.properties index 24a536f45dc..8088bf34d62 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:MaxPermSize=512m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae..41d9927a4d4 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 669386b870a..8fad3f5a98b 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json index 2327e470f54..2b9428a8c65 100644 --- a/android/link-assets-manifest.json +++ b/android/link-assets-manifest.json @@ -45,6 +45,54 @@ "path": "assets/fonts/TitilliumWeb/TitilliumWeb-SemiBoldItalic.ttf", "sha1": "f81d3a5f38c6bda1fb3547bf91fe0b68b54066c5" }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf", + "sha1": "becf8bd5053f5529785c9d1d975f2d6f54446d47" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf", + "sha1": "079c243911ef9d6cbb25f5e25b0a0fcdc80e217a" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf", + "sha1": "5348d5fdb96da25b0b444b6cf433100e17b2c944" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf", + "sha1": "19b6af581aa9de369c5551b5c81af4567e96fd33" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf", + "sha1": "149caa0987bb4765e34a34ccc315d89a9d5b259c" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf", + "sha1": "096f9b521b0fc0e7ebe6c60890fbfe312e7e4b01" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf", + "sha1": "9a74303f47edd127dc6f2c48301c9b0d4ffbbf17" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf", + "sha1": "0a8be559a9a9d8ce47eb6ca3de9db16f42a00580" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf", + "sha1": "fa6cff8c0046996bc9fb124abf5799408eb3b369" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf", + "sha1": "8502e9c211803bb2b0446348c69cf56763c987a3" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf", + "sha1": "dd3f889e6ca51de7e2006418b0125c02421fe574" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf", + "sha1": "fb0f0fcc215d93fd4241f3af18408b7c42b33b3d" + }, { "path": "assets/fonts/ReadexPro/ReadexPro-Regular.ttf", "sha1": "93e4080794b725f216a94b57ed62a51bc77bce91" diff --git a/android/settings.gradle b/android/settings.gradle index cf96c09a5b7..42a3d842c85 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -62,7 +62,5 @@ if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") include(":ReactAndroid:hermes-engine") project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') } -include ':@react-native-community_datetimepicker' -project(':@react-native-community_datetimepicker').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/datetimepicker/android') include ':react-native-art' project(':react-native-art').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/art/android') diff --git a/assets/fonts/TitilliumSansPro-OFL.txt b/assets/fonts/TitilliumSansPro-OFL.txt new file mode 100644 index 00000000000..5ffcfe18ea6 --- /dev/null +++ b/assets/fonts/TitilliumSansPro-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2024 The Titillium Pro Project Authors (https://github.com/chialab/titillium_pro) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf new file mode 100644 index 00000000000..3cd95e37eaf Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf new file mode 100644 index 00000000000..a6fd8bc8af7 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf new file mode 100644 index 00000000000..3d774ef1a03 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf new file mode 100644 index 00000000000..bdf1aaedfc9 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf new file mode 100644 index 00000000000..51f58e72f82 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf new file mode 100644 index 00000000000..c75c48c9763 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf new file mode 100644 index 00000000000..3146a822c96 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf new file mode 100644 index 00000000000..4bd7bcf2d5a Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf new file mode 100644 index 00000000000..fc7186dda82 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf new file mode 100644 index 00000000000..8242da46cf5 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf new file mode 100644 index 00000000000..e9f73560c42 Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf differ diff --git a/assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf b/assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf new file mode 100644 index 00000000000..869febf3eea Binary files /dev/null and b/assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf differ diff --git a/img/app-logo.svg b/img/app-logo.svg index 71c7b51020d..41663cea693 100644 --- a/img/app-logo.svg +++ b/img/app-logo.svg @@ -1,16 +1,9 @@ - - - - + + + + - - + + - - - - - - - diff --git a/img/cie/CIE-onboarding-illustration.png b/img/cie/CIE-onboarding-illustration.png deleted file mode 100644 index 3acf74bee10..00000000000 Binary files a/img/cie/CIE-onboarding-illustration.png and /dev/null differ diff --git a/img/cie/CIE-onboarding-illustration@2x.png b/img/cie/CIE-onboarding-illustration@2x.png deleted file mode 100644 index 3a1a7640204..00000000000 Binary files a/img/cie/CIE-onboarding-illustration@2x.png and /dev/null differ diff --git a/img/cie/CIE-onboarding-illustration@3x.png b/img/cie/CIE-onboarding-illustration@3x.png deleted file mode 100644 index 5d3a7890c1d..00000000000 Binary files a/img/cie/CIE-onboarding-illustration@3x.png and /dev/null differ diff --git a/img/error.png b/img/error.png deleted file mode 100644 index 55fab097d3e..00000000000 Binary files a/img/error.png and /dev/null differ diff --git a/img/features/cdc/bonus.svg b/img/features/cdc/bonus.svg deleted file mode 100644 index cfd865ed045..00000000000 --- a/img/features/cdc/bonus.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/img/features/cgn/cgn_card.svg b/img/features/cgn/cgn_card.svg new file mode 100644 index 00000000000..6494a89f257 --- /dev/null +++ b/img/features/cgn/cgn_card.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/img/features/idpay/bonus_bg_svg.svg b/img/features/idpay/bonus_bg_svg.svg deleted file mode 100644 index dba6c003dfd..00000000000 --- a/img/features/idpay/bonus_bg_svg.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/img/features/idpay/creditcard.svg b/img/features/idpay/creditcard.svg deleted file mode 100644 index fb1183a224f..00000000000 --- a/img/features/idpay/creditcard.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/img/features/idpay/empty_initiative.svg b/img/features/idpay/empty_initiative.svg deleted file mode 100644 index 5da49215e81..00000000000 --- a/img/features/idpay/empty_initiative.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/img/features/idpay/institution.svg b/img/features/idpay/institution.svg deleted file mode 100644 index 676d45e605b..00000000000 --- a/img/features/idpay/institution.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/img/features/idpay/wallet_card.svg b/img/features/idpay/wallet_card.svg new file mode 100644 index 00000000000..8d816bc7c21 --- /dev/null +++ b/img/features/idpay/wallet_card.svg @@ -0,0 +1,4 @@ + + + + diff --git a/img/features/payments/Amount.svg b/img/features/payments/Amount.svg deleted file mode 100644 index bbdfc07ae08..00000000000 --- a/img/features/payments/Amount.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/img/features/payments/Giacenza.svg b/img/features/payments/Giacenza.svg deleted file mode 100644 index 8ec5322da8f..00000000000 --- a/img/features/payments/Giacenza.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/img/features/payments/calendar.svg b/img/features/payments/calendar.svg deleted file mode 100644 index 7f0c8dc39b8..00000000000 --- a/img/features/payments/calendar.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/img/landing/01.png b/img/landing/01.png deleted file mode 100644 index b6fcef5ac73..00000000000 Binary files a/img/landing/01.png and /dev/null differ diff --git a/img/landing/02.png b/img/landing/02.png deleted file mode 100644 index 07d941b54ce..00000000000 Binary files a/img/landing/02.png and /dev/null differ diff --git a/img/landing/03.png b/img/landing/03.png deleted file mode 100644 index bad6fb3d47f..00000000000 Binary files a/img/landing/03.png and /dev/null differ diff --git a/img/landing/04.png b/img/landing/04.png deleted file mode 100644 index 78ef93000da..00000000000 Binary files a/img/landing/04.png and /dev/null differ diff --git a/img/landing/05.png b/img/landing/05.png deleted file mode 100644 index 64ac5687988..00000000000 Binary files a/img/landing/05.png and /dev/null differ diff --git a/img/landing/session_expired.png b/img/landing/session_expired.png deleted file mode 100644 index 4ad33dda6f7..00000000000 Binary files a/img/landing/session_expired.png and /dev/null differ diff --git a/img/logo-it-square-icon.png b/img/logo-it-square-icon.png deleted file mode 100644 index e12edad85ec..00000000000 Binary files a/img/logo-it-square-icon.png and /dev/null differ diff --git a/img/logo-it.png b/img/logo-it.png deleted file mode 100644 index d4a2a74b738..00000000000 Binary files a/img/logo-it.png and /dev/null differ diff --git a/img/pictograms/fireworks.png b/img/pictograms/fireworks.png deleted file mode 100644 index 0ce0e95f08c..00000000000 Binary files a/img/pictograms/fireworks.png and /dev/null differ diff --git a/img/pictograms/hourglass.png b/img/pictograms/hourglass.png deleted file mode 100644 index ad2d20d3262..00000000000 Binary files a/img/pictograms/hourglass.png and /dev/null differ diff --git a/img/pictograms/payment-completed.svg b/img/pictograms/payment-completed.svg deleted file mode 100644 index d9e061ce403..00000000000 --- a/img/pictograms/payment-completed.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - 686AD73B-1535-427E-9A17-D2F9A04978DB - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/img/rooted/broken-phone.png b/img/rooted/broken-phone.png deleted file mode 100644 index b98349e31a4..00000000000 Binary files a/img/rooted/broken-phone.png and /dev/null differ diff --git a/img/rooted/broken-phone@2x.png b/img/rooted/broken-phone@2x.png deleted file mode 100644 index 1bca58dad94..00000000000 Binary files a/img/rooted/broken-phone@2x.png and /dev/null differ diff --git a/img/rooted/broken-phone@3x.png b/img/rooted/broken-phone@3x.png deleted file mode 100644 index 7eed13352d5..00000000000 Binary files a/img/rooted/broken-phone@3x.png and /dev/null differ diff --git a/img/test/fingerprint.svg b/img/test/fingerprint.svg deleted file mode 100644 index e9b91053189..00000000000 --- a/img/test/fingerprint.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/img/wallet/errors/domain-unknown-icon.png b/img/wallet/errors/domain-unknown-icon.png deleted file mode 100755 index 4eb89cda1c0..00000000000 Binary files a/img/wallet/errors/domain-unknown-icon.png and /dev/null differ diff --git a/img/wallet/errors/domain-unknown-icon.svg b/img/wallet/errors/domain-unknown-icon.svg deleted file mode 100755 index eb69484a010..00000000000 --- a/img/wallet/errors/domain-unknown-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - A14060FF-4435-46E0-894B-36B05046628C - Created with sketchtool. - - - - - - \ No newline at end of file diff --git a/img/wallet/errors/domain-unknown-icon@2x.png b/img/wallet/errors/domain-unknown-icon@2x.png deleted file mode 100755 index 11d34bfbc37..00000000000 Binary files a/img/wallet/errors/domain-unknown-icon@2x.png and /dev/null differ diff --git a/img/wallet/errors/domain-unknown-icon@3x.png b/img/wallet/errors/domain-unknown-icon@3x.png deleted file mode 100755 index 222d19bb0c6..00000000000 Binary files a/img/wallet/errors/domain-unknown-icon@3x.png and /dev/null differ diff --git a/img/wallet/errors/invalid-amount-icon.png b/img/wallet/errors/invalid-amount-icon.png deleted file mode 100755 index 56ecd84761f..00000000000 Binary files a/img/wallet/errors/invalid-amount-icon.png and /dev/null differ diff --git a/img/wallet/errors/invalid-amount-icon.svg b/img/wallet/errors/invalid-amount-icon.svg deleted file mode 100755 index c05d74d2f17..00000000000 --- a/img/wallet/errors/invalid-amount-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - 56267611-7536-4E1D-B42D-210C092C1A4D - Created with sketchtool. - - - - - - \ No newline at end of file diff --git a/img/wallet/errors/invalid-amount-icon@2x.png b/img/wallet/errors/invalid-amount-icon@2x.png deleted file mode 100755 index d1daf05e8ff..00000000000 Binary files a/img/wallet/errors/invalid-amount-icon@2x.png and /dev/null differ diff --git a/img/wallet/errors/invalid-amount-icon@3x.png b/img/wallet/errors/invalid-amount-icon@3x.png deleted file mode 100755 index 40593f510a0..00000000000 Binary files a/img/wallet/errors/invalid-amount-icon@3x.png and /dev/null differ diff --git a/img/wallet/errors/missing-payment-id-icon.png b/img/wallet/errors/missing-payment-id-icon.png deleted file mode 100755 index 54c6b853368..00000000000 Binary files a/img/wallet/errors/missing-payment-id-icon.png and /dev/null differ diff --git a/img/wallet/errors/missing-payment-id-icon.svg b/img/wallet/errors/missing-payment-id-icon.svg deleted file mode 100755 index 9db4b1c20b6..00000000000 --- a/img/wallet/errors/missing-payment-id-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - AE37566F-F38B-4066-8A01-2481839012A3 - Created with sketchtool. - - - - - - \ No newline at end of file diff --git a/img/wallet/errors/missing-payment-id-icon@2x.png b/img/wallet/errors/missing-payment-id-icon@2x.png deleted file mode 100755 index cc2b7a1662e..00000000000 Binary files a/img/wallet/errors/missing-payment-id-icon@2x.png and /dev/null differ diff --git a/img/wallet/errors/missing-payment-id-icon@3x.png b/img/wallet/errors/missing-payment-id-icon@3x.png deleted file mode 100755 index 4d439e33570..00000000000 Binary files a/img/wallet/errors/missing-payment-id-icon@3x.png and /dev/null differ diff --git a/img/wallet/errors/payment-duplicated-icon.png b/img/wallet/errors/payment-duplicated-icon.png deleted file mode 100755 index f339cabe4e1..00000000000 Binary files a/img/wallet/errors/payment-duplicated-icon.png and /dev/null differ diff --git a/img/wallet/errors/payment-duplicated-icon.svg b/img/wallet/errors/payment-duplicated-icon.svg deleted file mode 100755 index 82727b8ce5c..00000000000 --- a/img/wallet/errors/payment-duplicated-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - 48E36FE0-0865-477B-B31D-3066D01401D9 - Created with sketchtool. - - - - - - \ No newline at end of file diff --git a/img/wallet/errors/payment-duplicated-icon@2x.png b/img/wallet/errors/payment-duplicated-icon@2x.png deleted file mode 100755 index 2ea734fc91f..00000000000 Binary files a/img/wallet/errors/payment-duplicated-icon@2x.png and /dev/null differ diff --git a/img/wallet/errors/payment-duplicated-icon@3x.png b/img/wallet/errors/payment-duplicated-icon@3x.png deleted file mode 100755 index 67ff5cf8bb4..00000000000 Binary files a/img/wallet/errors/payment-duplicated-icon@3x.png and /dev/null differ diff --git a/img/wallet/errors/payment-ongoing-icon.png b/img/wallet/errors/payment-ongoing-icon.png deleted file mode 100755 index b9dea78b5e2..00000000000 Binary files a/img/wallet/errors/payment-ongoing-icon.png and /dev/null differ diff --git a/img/wallet/errors/payment-ongoing-icon.svg b/img/wallet/errors/payment-ongoing-icon.svg deleted file mode 100755 index 75309723227..00000000000 --- a/img/wallet/errors/payment-ongoing-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - B72CED6E-F033-4114-9030-C2BB7AAEB8F9 - Created with sketchtool. - - - - - - \ No newline at end of file diff --git a/img/wallet/errors/payment-ongoing-icon@2x.png b/img/wallet/errors/payment-ongoing-icon@2x.png deleted file mode 100755 index e98f8fbb017..00000000000 Binary files a/img/wallet/errors/payment-ongoing-icon@2x.png and /dev/null differ diff --git a/img/wallet/errors/payment-ongoing-icon@3x.png b/img/wallet/errors/payment-ongoing-icon@3x.png deleted file mode 100755 index 06764f5a89a..00000000000 Binary files a/img/wallet/errors/payment-ongoing-icon@3x.png and /dev/null differ diff --git a/img/wallet/initiatives.svg b/img/wallet/initiatives.svg deleted file mode 100644 index 355d5217c8f..00000000000 --- a/img/wallet/initiatives.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/img/wallet/logo-pagopa-test.png b/img/wallet/logo-pagopa-test.png deleted file mode 100644 index 83c44b1361f..00000000000 Binary files a/img/wallet/logo-pagopa-test.png and /dev/null differ diff --git a/img/wallet/logo-pagopa.png b/img/wallet/logo-pagopa.png deleted file mode 100644 index d653cf5ffa5..00000000000 Binary files a/img/wallet/logo-pagopa.png and /dev/null differ diff --git a/img/wallet/payment-methods/paypal/paypal_logo_ext.svg b/img/wallet/payment-methods/paypal/paypal_logo_ext.svg new file mode 100644 index 00000000000..fff548e5d45 --- /dev/null +++ b/img/wallet/payment-methods/paypal/paypal_logo_ext.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/wallet/payment-methods/postepay-logo.png b/img/wallet/payment-methods/postepay-logo.png deleted file mode 100644 index a69b231bd6e..00000000000 Binary files a/img/wallet/payment-methods/postepay-logo.png and /dev/null differ diff --git a/img/wallet/payment-methods/satispay-logo.png b/img/wallet/payment-methods/satispay-logo.png deleted file mode 100644 index 479246b4a0c..00000000000 Binary files a/img/wallet/payment-methods/satispay-logo.png and /dev/null differ diff --git a/img/wallet/payment-methods/satispay-logo.svg b/img/wallet/payment-methods/satispay-logo.svg deleted file mode 100644 index 7c50b7c675a..00000000000 --- a/img/wallet/payment-methods/satispay-logo.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/img/wallet/payment-methods/venpay-logo.png b/img/wallet/payment-methods/venpay-logo.png deleted file mode 100644 index 44a14ca7e87..00000000000 Binary files a/img/wallet/payment-methods/venpay-logo.png and /dev/null differ diff --git a/img/wallet/unknown-gdo-2x.png b/img/wallet/unknown-gdo-2x.png deleted file mode 100644 index 04a01915fb6..00000000000 Binary files a/img/wallet/unknown-gdo-2x.png and /dev/null differ diff --git a/img/wallet/unknown-gdo-primary.svg b/img/wallet/unknown-gdo-primary.svg deleted file mode 100644 index 407b5d8004c..00000000000 --- a/img/wallet/unknown-gdo-primary.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/img/wallet/unknown-gdo.png b/img/wallet/unknown-gdo.png deleted file mode 100644 index 904fa267e25..00000000000 Binary files a/img/wallet/unknown-gdo.png and /dev/null differ diff --git a/img/wallet/unknown-gdo.svg b/img/wallet/unknown-gdo.svg deleted file mode 100644 index f8a31fdcc24..00000000000 --- a/img/wallet/unknown-gdo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - unknown-gdo - - - - - - - - - - \ No newline at end of file diff --git a/ios/ItaliaApp.xcodeproj/project.pbxproj b/ios/ItaliaApp.xcodeproj/project.pbxproj index e625fd411f9..2a640f8fb6b 100644 --- a/ios/ItaliaApp.xcodeproj/project.pbxproj +++ b/ios/ItaliaApp.xcodeproj/project.pbxproj @@ -14,15 +14,16 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 16E38CBD19D4437A9C020B21 /* RobotoMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 76E4CEA78E3F4060ABB59808 /* RobotoMono-Regular.ttf */; }; 194A5D2B1F027F5A0078620E /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = 194A5D2A1F027F5A0078620E /* Podfile */; }; - 1CD9CAEA755BFC30AE893F46 /* libPods-ItaliaApp-ItaliaAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A0A336CDDAE56472931AD9F /* libPods-ItaliaApp-ItaliaAppTests.a */; }; 28E4866232804051B865FC10 /* RobotoMono-RegularItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5389C36762A04AE69D762289 /* RobotoMono-RegularItalic.ttf */; }; 2AD40B9924CB049500E4124A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2AD40B9824CB049500E4124A /* LaunchScreen.storyboard */; }; 3A3CA4696BA44782A02FE37E /* TitilliumWeb-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B021D4CC17ED4D54B7944FED /* TitilliumWeb-SemiBold.ttf */; }; 4D331CF68DAC4800A6D6297A /* TitilliumWeb-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 11C2483C363E432BB4D0A583 /* TitilliumWeb-Regular.ttf */; }; + 544D7A6A6990E1AD0E113BF2 /* libPods-ItaliaApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A88AAE92C2AA92D33DB25F0C /* libPods-ItaliaApp.a */; }; 570BBC8FC70C4A76ABF035AC /* TitilliumWeb-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 163BC670599B42509D4D138E /* TitilliumWeb-Light.ttf */; }; 69A5E1839C924EE5B8CD9470 /* ReadexPro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2C584AD92CB24628B1AA76DE /* ReadexPro-Regular.ttf */; }; 6B44A74498354CC19B90FE9C /* TitilliumWeb-ExtraLightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6754714BBEF54AA59833C0EB /* TitilliumWeb-ExtraLightItalic.ttf */; }; 72E4B1EFF7D4414483079F91 /* TitilliumWeb-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CED8B1FF1E254583BB3F285F /* TitilliumWeb-SemiBoldItalic.ttf */; }; + 7578F223A01E0E15E7072DAF /* libPods-ItaliaApp-ItaliaAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59BCC34321A36ABA56F255E3 /* libPods-ItaliaApp-ItaliaAppTests.a */; }; 859781B477BB45579FB9A9F2 /* TitilliumWeb-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F709D4AE20E44EFAAB255A8E /* TitilliumWeb-Italic.ttf */; }; 862C0E693C864E76BADCDFCC /* TitilliumWeb-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6A786CC45B2D4B77BDF55C1D /* TitilliumWeb-Black.ttf */; }; 8788744508D34396942D3DB4 /* RobotoMono-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AFE8D23873DE458BB54EB46B /* RobotoMono-Light.ttf */; }; @@ -33,11 +34,22 @@ 9975E38DE95949D28D07A275 /* RobotoMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B7088D9B42BA4275A767BEFF /* RobotoMono-Bold.ttf */; }; BE1A42156484487BABBC4DED /* TitilliumWeb-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 309BA0207AA24FE9A0EBE61C /* TitilliumWeb-Bold.ttf */; }; C2CF038002D24FEEAB41B336 /* RobotoMono-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 640E574519CA4183B230AF22 /* RobotoMono-LightItalic.ttf */; }; - CD4FC1EC91E60128F3E01292 /* libPods-ItaliaApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A6F417D3B8F84EA607A5C6F3 /* libPods-ItaliaApp.a */; }; D18E075B28304466B6CA2381 /* TitilliumWeb-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B65D221FCB5A461FBC44285D /* TitilliumWeb-LightItalic.ttf */; }; EFFA620FCD2D46F6B942663B /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2D52800996AA471A87A7F975 /* LICENSE.txt */; }; F0625E7F2326825600EDEF90 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED297162215061F000B7C4FE /* JavaScriptCore.framework */; }; F09FEB3C231818E3007071DB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F09FEB0D231818E3007071DB /* Localizable.strings */; }; + F2B1630E2B753C9D00F6E487 /* TitilliumSansPro-Black.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163022B753C9D00F6E487 /* TitilliumSansPro-Black.otf */; }; + F2B1630F2B753C9D00F6E487 /* TitilliumSansPro-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163032B753C9D00F6E487 /* TitilliumSansPro-Semibold.otf */; }; + F2B163102B753C9D00F6E487 /* TitilliumSansPro-LightItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163042B753C9D00F6E487 /* TitilliumSansPro-LightItalic.otf */; }; + F2B163112B753C9D00F6E487 /* TitilliumSansPro-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163052B753C9D00F6E487 /* TitilliumSansPro-Italic.otf */; }; + F2B163122B753C9D00F6E487 /* TitilliumSansPro-SemiboldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163062B753C9D00F6E487 /* TitilliumSansPro-SemiboldItalic.otf */; }; + F2B163132B753C9D00F6E487 /* TitilliumSansPro-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163072B753C9D00F6E487 /* TitilliumSansPro-Thin.otf */; }; + F2B163142B753C9D00F6E487 /* TitilliumSansPro-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163082B753C9D00F6E487 /* TitilliumSansPro-BoldItalic.otf */; }; + F2B163152B753C9D00F6E487 /* TitilliumSansPro-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B163092B753C9D00F6E487 /* TitilliumSansPro-Light.otf */; }; + F2B163162B753C9D00F6E487 /* TitilliumSansPro-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B1630A2B753C9D00F6E487 /* TitilliumSansPro-Regular.otf */; }; + F2B163172B753C9D00F6E487 /* TitilliumSansPro-BlackItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B1630B2B753C9D00F6E487 /* TitilliumSansPro-BlackItalic.otf */; }; + F2B163182B753C9D00F6E487 /* TitilliumSansPro-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B1630C2B753C9D00F6E487 /* TitilliumSansPro-Bold.otf */; }; + F2B163192B753C9D00F6E487 /* TitilliumSansPro-ThinItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = F2B1630D2B753C9D00F6E487 /* TitilliumSansPro-ThinItalic.otf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,6 +80,7 @@ 00E356EE1AD99517003FC87E /* ItaliaAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ItaliaAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* ItaliaAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ItaliaAppTests.m; sourceTree = ""; }; + 058F4F4927B1B1C84C7F1137 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp-ItaliaAppTests.release.xcconfig"; path = "Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests.release.xcconfig"; sourceTree = ""; }; 11C2483C363E432BB4D0A583 /* TitilliumWeb-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-Regular.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-Regular.ttf"; sourceTree = ""; }; 133638FB213D788900B0C079 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 133638FD213D78DB00B0C079 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -86,23 +99,25 @@ 2C584AD92CB24628B1AA76DE /* ReadexPro-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ReadexPro-Regular.ttf"; path = "../assets/fonts/ReadexPro/ReadexPro-Regular.ttf"; sourceTree = ""; }; 2D52800996AA471A87A7F975 /* LICENSE.txt */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = LICENSE.txt; path = ../assets/fonts/RobotoMono/LICENSE.txt; sourceTree = ""; }; 309BA0207AA24FE9A0EBE61C /* TitilliumWeb-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-Bold.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-Bold.ttf"; sourceTree = ""; }; - 3A0A336CDDAE56472931AD9F /* libPods-ItaliaApp-ItaliaAppTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ItaliaApp-ItaliaAppTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4F044D1214BF98AC6510D261 /* Pods-ItaliaApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp.debug.xcconfig"; path = "Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp.debug.xcconfig"; sourceTree = ""; }; 5389C36762A04AE69D762289 /* RobotoMono-RegularItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "RobotoMono-RegularItalic.ttf"; path = "../assets/fonts/RobotoMono/RobotoMono-RegularItalic.ttf"; sourceTree = ""; }; + 59BCC34321A36ABA56F255E3 /* libPods-ItaliaApp-ItaliaAppTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ItaliaApp-ItaliaAppTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 640E574519CA4183B230AF22 /* RobotoMono-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "RobotoMono-LightItalic.ttf"; path = "../assets/fonts/RobotoMono/RobotoMono-LightItalic.ttf"; sourceTree = ""; }; 6754714BBEF54AA59833C0EB /* TitilliumWeb-ExtraLightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-ExtraLightItalic.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-ExtraLightItalic.ttf"; sourceTree = ""; }; 6A786CC45B2D4B77BDF55C1D /* TitilliumWeb-Black.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-Black.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-Black.ttf"; sourceTree = ""; }; 6C4A0A2AB90C8575993DF1E1 /* Instabug.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Instabug.framework; path = "../node_modules/instabug-reactnative/ios/Instabug.framework"; sourceTree = ""; }; + 6D0115BDF703AEBFD8B86A12 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig"; path = "Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig"; sourceTree = ""; }; + 6F8C6AF94C801CE01EC3D81A /* Pods-ItaliaApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp.release.xcconfig"; path = "Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp.release.xcconfig"; sourceTree = ""; }; + 722056DF161D843DD91D88A7 /* Pods-ItaliaApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp.debug.xcconfig"; path = "Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp.debug.xcconfig"; sourceTree = ""; }; 76E4CEA78E3F4060ABB59808 /* RobotoMono-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "RobotoMono-Regular.ttf"; path = "../assets/fonts/RobotoMono/RobotoMono-Regular.ttf"; sourceTree = ""; }; 7A83F0572152B12C000C6389 /* ItaliaApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ItaliaApp.entitlements; path = ItaliaApp/ItaliaApp.entitlements; sourceTree = ""; }; 875B0C3A5326413494A9311A /* DMMono-Medium.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DMMono-Medium.ttf"; path = "../assets/fonts/DMMono/DMMono-Medium.ttf"; sourceTree = ""; }; - A6F417D3B8F84EA607A5C6F3 /* libPods-ItaliaApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ItaliaApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - AE2A89F5F6A57BDBD22D81D9 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp-ItaliaAppTests.release.xcconfig"; path = "Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests.release.xcconfig"; sourceTree = ""; }; + A88AAE92C2AA92D33DB25F0C /* libPods-ItaliaApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ItaliaApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; AFE8D23873DE458BB54EB46B /* RobotoMono-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "RobotoMono-Light.ttf"; path = "../assets/fonts/RobotoMono/RobotoMono-Light.ttf"; sourceTree = ""; }; B021D4CC17ED4D54B7944FED /* TitilliumWeb-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-SemiBold.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-SemiBold.ttf"; sourceTree = ""; }; + B57593872B70FFA400674515 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + B57593882B70FFA400674515 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = ItaliaApp/de.lproj/Localizable.strings; sourceTree = ""; }; B65D221FCB5A461FBC44285D /* TitilliumWeb-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-LightItalic.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-LightItalic.ttf"; sourceTree = ""; }; B7088D9B42BA4275A767BEFF /* RobotoMono-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "RobotoMono-Bold.ttf"; path = "../assets/fonts/RobotoMono/RobotoMono-Bold.ttf"; sourceTree = ""; }; - B7EF7760029EB39299B837EA /* Pods-ItaliaApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp.release.xcconfig"; path = "Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp.release.xcconfig"; sourceTree = ""; }; CED8B1FF1E254583BB3F285F /* TitilliumWeb-SemiBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-SemiBoldItalic.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-SemiBoldItalic.ttf"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; EE4F83DB63254F5285C36A40 /* TitilliumWeb-ExtraLight.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-ExtraLight.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-ExtraLight.ttf"; sourceTree = ""; }; @@ -110,8 +125,19 @@ F0625E7B2326822400EDEF90 /* libReact-RCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libReact-RCTImage.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F09FEB0E231818E3007071DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = ItaliaApp/en.lproj/Localizable.strings; sourceTree = ""; }; F09FEB3D231818F0007071DB /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = ItaliaApp/it.lproj/Localizable.strings; sourceTree = ""; }; + F2B163022B753C9D00F6E487 /* TitilliumSansPro-Black.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Black.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf"; sourceTree = ""; }; + F2B163032B753C9D00F6E487 /* TitilliumSansPro-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Semibold.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf"; sourceTree = ""; }; + F2B163042B753C9D00F6E487 /* TitilliumSansPro-LightItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-LightItalic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf"; sourceTree = ""; }; + F2B163052B753C9D00F6E487 /* TitilliumSansPro-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Italic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf"; sourceTree = ""; }; + F2B163062B753C9D00F6E487 /* TitilliumSansPro-SemiboldItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-SemiboldItalic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf"; sourceTree = ""; }; + F2B163072B753C9D00F6E487 /* TitilliumSansPro-Thin.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Thin.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf"; sourceTree = ""; }; + F2B163082B753C9D00F6E487 /* TitilliumSansPro-BoldItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-BoldItalic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf"; sourceTree = ""; }; + F2B163092B753C9D00F6E487 /* TitilliumSansPro-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Light.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf"; sourceTree = ""; }; + F2B1630A2B753C9D00F6E487 /* TitilliumSansPro-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Regular.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf"; sourceTree = ""; }; + F2B1630B2B753C9D00F6E487 /* TitilliumSansPro-BlackItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-BlackItalic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf"; sourceTree = ""; }; + F2B1630C2B753C9D00F6E487 /* TitilliumSansPro-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-Bold.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf"; sourceTree = ""; }; + F2B1630D2B753C9D00F6E487 /* TitilliumSansPro-ThinItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "TitilliumSansPro-ThinItalic.otf"; path = "../assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf"; sourceTree = ""; }; F709D4AE20E44EFAAB255A8E /* TitilliumWeb-Italic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "TitilliumWeb-Italic.ttf"; path = "../assets/fonts/TitilliumWeb/TitilliumWeb-Italic.ttf"; sourceTree = ""; }; - FCE075AF22EBCD57D83AD630 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig"; path = "Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -119,7 +145,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1CD9CAEA755BFC30AE893F46 /* libPods-ItaliaApp-ItaliaAppTests.a in Frameworks */, + 7578F223A01E0E15E7072DAF /* libPods-ItaliaApp-ItaliaAppTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -128,7 +154,7 @@ buildActionMask = 2147483647; files = ( F0625E7F2326825600EDEF90 /* JavaScriptCore.framework in Frameworks */, - CD4FC1EC91E60128F3E01292 /* libPods-ItaliaApp.a in Frameworks */, + 544D7A6A6990E1AD0E113BF2 /* libPods-ItaliaApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -181,6 +207,18 @@ 36F229EFB63F4CD98EF610EC /* Resources */ = { isa = PBXGroup; children = ( + F2B163022B753C9D00F6E487 /* TitilliumSansPro-Black.otf */, + F2B1630B2B753C9D00F6E487 /* TitilliumSansPro-BlackItalic.otf */, + F2B1630C2B753C9D00F6E487 /* TitilliumSansPro-Bold.otf */, + F2B163082B753C9D00F6E487 /* TitilliumSansPro-BoldItalic.otf */, + F2B163052B753C9D00F6E487 /* TitilliumSansPro-Italic.otf */, + F2B163092B753C9D00F6E487 /* TitilliumSansPro-Light.otf */, + F2B163042B753C9D00F6E487 /* TitilliumSansPro-LightItalic.otf */, + F2B1630A2B753C9D00F6E487 /* TitilliumSansPro-Regular.otf */, + F2B163032B753C9D00F6E487 /* TitilliumSansPro-Semibold.otf */, + F2B163062B753C9D00F6E487 /* TitilliumSansPro-SemiboldItalic.otf */, + F2B163072B753C9D00F6E487 /* TitilliumSansPro-Thin.otf */, + F2B1630D2B753C9D00F6E487 /* TitilliumSansPro-ThinItalic.otf */, 6A786CC45B2D4B77BDF55C1D /* TitilliumWeb-Black.ttf */, 309BA0207AA24FE9A0EBE61C /* TitilliumWeb-Bold.ttf */, 1E4EECD1122B401CAC190472 /* TitilliumWeb-BoldItalic.ttf */, @@ -208,10 +246,10 @@ 591602A69619994F43D34A93 /* Pods */ = { isa = PBXGroup; children = ( - 4F044D1214BF98AC6510D261 /* Pods-ItaliaApp.debug.xcconfig */, - B7EF7760029EB39299B837EA /* Pods-ItaliaApp.release.xcconfig */, - FCE075AF22EBCD57D83AD630 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */, - AE2A89F5F6A57BDBD22D81D9 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */, + 722056DF161D843DD91D88A7 /* Pods-ItaliaApp.debug.xcconfig */, + 6F8C6AF94C801CE01EC3D81A /* Pods-ItaliaApp.release.xcconfig */, + 6D0115BDF703AEBFD8B86A12 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */, + 058F4F4927B1B1C84C7F1137 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -256,8 +294,8 @@ F0625E7B2326822400EDEF90 /* libReact-RCTImage.a */, F0625E792326820B00EDEF90 /* libReact-RCTImage.a */, 6C4A0A2AB90C8575993DF1E1 /* Instabug.framework */, - A6F417D3B8F84EA607A5C6F3 /* libPods-ItaliaApp.a */, - 3A0A336CDDAE56472931AD9F /* libPods-ItaliaApp-ItaliaAppTests.a */, + A88AAE92C2AA92D33DB25F0C /* libPods-ItaliaApp.a */, + 59BCC34321A36ABA56F255E3 /* libPods-ItaliaApp-ItaliaAppTests.a */, ); name = Frameworks; sourceTree = ""; @@ -269,12 +307,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ItaliaAppTests" */; buildPhases = ( - CC25A055C342FAAE3D4A7914 /* [CP] Check Pods Manifest.lock */, + 352F3B8D397ADE20251EED97 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - 798F586C1A40477B6340BEA7 /* [CP] Embed Pods Frameworks */, - F162F3AB6F2574EDC5D74D51 /* [CP] Copy Pods Resources */, + F08399D3BEE675C55F82C6BE /* [CP] Embed Pods Frameworks */, + 68F41393BCCEDD7F8C939A70 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -290,7 +328,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ItaliaApp" */; buildPhases = ( - 02908AF96918A81891FE2AAA /* [CP] Check Pods Manifest.lock */, + CC23EFFCB8098D26BE7DBAA5 /* [CP] Check Pods Manifest.lock */, 95AEBF4A23D0A295000598A9 /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, @@ -298,8 +336,8 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 61CC933C2135818C00206602 /* Embed Frameworks */, 7A034949213D55CA0064B689 /* Work around InputMask.xcodeproj embedding an extra set of Swift libraries */, - 78409FB602D7E5A8BB0D890D /* [CP] Embed Pods Frameworks */, - 93CC51473253477B8DB84D24 /* [CP] Copy Pods Resources */, + 520BD832C8035EF5E68D9913 /* [CP] Embed Pods Frameworks */, + 963519164D766C5238AA1B42 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -344,6 +382,7 @@ en, Base, it, + de, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -370,7 +409,9 @@ files = ( 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 133638FA213D788900B0C079 /* InfoPlist.strings in Resources */, + F2B163152B753C9D00F6E487 /* TitilliumSansPro-Light.otf in Resources */, 194A5D2B1F027F5A0078620E /* Podfile in Resources */, + F2B1630F2B753C9D00F6E487 /* TitilliumSansPro-Semibold.otf in Resources */, 862C0E693C864E76BADCDFCC /* TitilliumWeb-Black.ttf in Resources */, BE1A42156484487BABBC4DED /* TitilliumWeb-Bold.ttf in Resources */, 8D109BF3B47F46FDA6B105CE /* TitilliumWeb-BoldItalic.ttf in Resources */, @@ -379,18 +420,28 @@ 859781B477BB45579FB9A9F2 /* TitilliumWeb-Italic.ttf in Resources */, 2AD40B9924CB049500E4124A /* LaunchScreen.storyboard in Resources */, F09FEB3C231818E3007071DB /* Localizable.strings in Resources */, + F2B163162B753C9D00F6E487 /* TitilliumSansPro-Regular.otf in Resources */, 570BBC8FC70C4A76ABF035AC /* TitilliumWeb-Light.ttf in Resources */, D18E075B28304466B6CA2381 /* TitilliumWeb-LightItalic.ttf in Resources */, + F2B163102B753C9D00F6E487 /* TitilliumSansPro-LightItalic.otf in Resources */, + F2B163182B753C9D00F6E487 /* TitilliumSansPro-Bold.otf in Resources */, 4D331CF68DAC4800A6D6297A /* TitilliumWeb-Regular.ttf in Resources */, 3A3CA4696BA44782A02FE37E /* TitilliumWeb-SemiBold.ttf in Resources */, 72E4B1EFF7D4414483079F91 /* TitilliumWeb-SemiBoldItalic.ttf in Resources */, + F2B163192B753C9D00F6E487 /* TitilliumSansPro-ThinItalic.otf in Resources */, + F2B163112B753C9D00F6E487 /* TitilliumSansPro-Italic.otf in Resources */, + F2B163172B753C9D00F6E487 /* TitilliumSansPro-BlackItalic.otf in Resources */, EFFA620FCD2D46F6B942663B /* LICENSE.txt in Resources */, 9975E38DE95949D28D07A275 /* RobotoMono-Bold.ttf in Resources */, 8E7BB01F1A6D4D79B17B4210 /* RobotoMono-BoldItalic.ttf in Resources */, 8788744508D34396942D3DB4 /* RobotoMono-Light.ttf in Resources */, + F2B1630E2B753C9D00F6E487 /* TitilliumSansPro-Black.otf in Resources */, + F2B163132B753C9D00F6E487 /* TitilliumSansPro-Thin.otf in Resources */, C2CF038002D24FEEAB41B336 /* RobotoMono-LightItalic.ttf in Resources */, 16E38CBD19D4437A9C020B21 /* RobotoMono-Regular.ttf in Resources */, 28E4866232804051B865FC10 /* RobotoMono-RegularItalic.ttf in Resources */, + F2B163122B753C9D00F6E487 /* TitilliumSansPro-SemiboldItalic.otf in Resources */, + F2B163142B753C9D00F6E487 /* TitilliumSansPro-BoldItalic.otf in Resources */, 69A5E1839C924EE5B8CD9470 /* ReadexPro-Regular.ttf in Resources */, 97F089B39469411991377615 /* DMMono-Medium.ttf in Resources */, ); @@ -413,7 +464,7 @@ shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; - 02908AF96918A81891FE2AAA /* [CP] Check Pods Manifest.lock */ = { + 352F3B8D397ADE20251EED97 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -428,14 +479,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ItaliaApp-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ItaliaApp-ItaliaAppTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 78409FB602D7E5A8BB0D890D /* [CP] Embed Pods Frameworks */ = { + 520BD832C8035EF5E68D9913 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -481,50 +532,22 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 798F586C1A40477B6340BEA7 /* [CP] Embed Pods Frameworks */ = { + 68F41393BCCEDD7F8C939A70 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskAnswerBotProvidersSDK/AnswerBotProvidersSDK.framework/AnswerBotProvidersSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskAnswerBotSDK/AnswerBotSDK.framework/AnswerBotSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskChatProvidersSDK/ChatProvidersSDK.framework/ChatProvidersSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskChatSDK/ChatSDK.framework/ChatSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskCommonUISDK/CommonUISDK.framework/CommonUISDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskCoreSDK/ZendeskCoreSDK.framework/ZendeskCoreSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskMessagingAPISDK/MessagingAPI.framework/MessagingAPI", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskMessagingSDK/MessagingSDK.framework/MessagingSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSDKConfigurationsSDK/SDKConfigurations.framework/SDKConfigurations", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSupportProvidersSDK/SupportProvidersSDK.framework/SupportProvidersSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSupportSDK/SupportSDK.framework/SupportSDK", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes", + "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AnswerBotProvidersSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AnswerBotSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ChatProvidersSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ChatSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CommonUISDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZendeskCoreSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessagingAPI.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessagingSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDKConfigurations.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SupportProvidersSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SupportSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; 7A034949213D55CA0064B689 /* Work around InputMask.xcodeproj embedding an extra set of Swift libraries */ = { @@ -541,77 +564,43 @@ shellPath = /bin/sh; shellScript = "# see https://github.com/react-native-community/react-native-text-input-mask/issues/22#issuecomment-344765116\n\nEXTRA_DIR=\"${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Frameworks/InputMask.framework/Frameworks\"\n\nif [[ -d \"${EXTRA_DIR}\" ]]; then\n rm -rf \"${EXTRA_DIR}\"\nfi\n"; }; - 93CC51473253477B8DB84D24 /* [CP] Copy Pods Resources */ = { + 95AEBF4A23D0A295000598A9 /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp-resources.sh", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowLeft.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowLeft@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowRight.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowRight@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCheckmark.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCheckmark@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton@3x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPDismissKeyboard.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPDismissKeyboard@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPLogo.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPLogo@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/placeholder-image.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~ipad.xib", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~iphonelandscape.xib", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~iphoneportrait.xib", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", ); - name = "[CP] Copy Pods Resources"; + name = "Start Packager"; + outputFileListPaths = ( + ); outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowLeft.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowLeft@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowRight.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowRight@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCheckmark.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCheckmark@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton@3x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPDismissKeyboard.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPDismissKeyboard@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPLogo.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPLogo@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/placeholder-image.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~ipad.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~iphonelandscape.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~iphoneportrait.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\nif nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\nif ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\necho \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\nexit 2\nfi\nelse\nopen \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\nfi\nfi\n"; }; - 95AEBF4A23D0A295000598A9 /* Start Packager */ = { + 963519164D766C5238AA1B42 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", ); - name = "Start Packager"; - outputFileListPaths = ( - ); + name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\nif nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\nif ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\necho \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\nexit 2\nfi\nelse\nopen \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\nfi\nfi\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp/Pods-ItaliaApp-resources.sh\"\n"; + showEnvVarsInLog = 0; }; - CC25A055C342FAAE3D4A7914 /* [CP] Check Pods Manifest.lock */ = { + CC23EFFCB8098D26BE7DBAA5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -626,63 +615,57 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ItaliaApp-ItaliaAppTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ItaliaApp-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F162F3AB6F2574EDC5D74D51 /* [CP] Copy Pods Resources */ = { + F08399D3BEE675C55F82C6BE /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-resources.sh", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowLeft.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowLeft@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowRight.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPArrowRight@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCheckmark.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCheckmark@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPCloseButton@3x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPDismissKeyboard.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPDismissKeyboard@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPLogo.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/MPLogo@2x.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/Images/placeholder-image.png", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~ipad.xib", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~iphonelandscape.xib", - "${PODS_ROOT}/Mixpanel/Mixpanel/MPTakeoverNotificationViewController~iphoneportrait.xib", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + "${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskAnswerBotProvidersSDK/AnswerBotProvidersSDK.framework/AnswerBotProvidersSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskAnswerBotSDK/AnswerBotSDK.framework/AnswerBotSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskChatProvidersSDK/ChatProvidersSDK.framework/ChatProvidersSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskChatSDK/ChatSDK.framework/ChatSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskCommonUISDK/CommonUISDK.framework/CommonUISDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskCoreSDK/ZendeskCoreSDK.framework/ZendeskCoreSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskMessagingAPISDK/MessagingAPI.framework/MessagingAPI", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskMessagingSDK/MessagingSDK.framework/MessagingSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSDKConfigurationsSDK/SDKConfigurations.framework/SDKConfigurations", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSupportProvidersSDK/SupportProvidersSDK.framework/SupportProvidersSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ZendeskSupportSDK/SupportSDK.framework/SupportSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowLeft.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowLeft@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowRight.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPArrowRight@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCheckmark.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCheckmark@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPCloseButton@3x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPDismissKeyboard.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPDismissKeyboard@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPLogo.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPLogo@2x.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/placeholder-image.png", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~ipad.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~iphonelandscape.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MPTakeoverNotificationViewController~iphoneportrait.nib", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AnswerBotProvidersSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AnswerBotSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ChatProvidersSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ChatSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CommonUISDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZendeskCoreSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessagingAPI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessagingSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDKConfigurations.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SupportProvidersSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SupportSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ItaliaApp-ItaliaAppTests/Pods-ItaliaApp-ItaliaAppTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -721,6 +704,7 @@ children = ( 133638FB213D788900B0C079 /* en */, 133638FD213D78DB00B0C079 /* it */, + B57593872B70FFA400674515 /* de */, ); name = InfoPlist.strings; path = ItaliaApp; @@ -731,6 +715,7 @@ children = ( F09FEB0E231818E3007071DB /* en */, F09FEB3D231818F0007071DB /* it */, + B57593882B70FFA400674515 /* de */, ); name = Localizable.strings; sourceTree = ""; @@ -740,7 +725,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FCE075AF22EBCD57D83AD630 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */; + baseConfigurationReference = 6D0115BDF703AEBFD8B86A12 /* Pods-ItaliaApp-ItaliaAppTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = M2X5YQ4BJ7; @@ -752,7 +737,7 @@ INFOPLIST_FILE = ItaliaAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", @@ -765,7 +750,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AE2A89F5F6A57BDBD22D81D9 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */; + baseConfigurationReference = 058F4F4927B1B1C84C7F1137 /* Pods-ItaliaApp-ItaliaAppTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; @@ -774,7 +759,7 @@ INFOPLIST_FILE = ItaliaAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", @@ -787,7 +772,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4F044D1214BF98AC6510D261 /* Pods-ItaliaApp.debug.xcconfig */; + baseConfigurationReference = 722056DF161D843DD91D88A7 /* Pods-ItaliaApp.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -828,7 +813,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B7EF7760029EB39299B837EA /* Pods-ItaliaApp.release.xcconfig */; + baseConfigurationReference = 6F8C6AF94C801CE01EC3D81A /* Pods-ItaliaApp.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -871,7 +856,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -905,6 +890,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -922,6 +908,11 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -932,7 +923,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -962,6 +953,10 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -976,6 +971,11 @@ "\"$(SDKROOT)/usr/lib/swift\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/Contents.json b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/Contents.json index 98fd313ae23..35e7856f312 100644 --- a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/Contents.json +++ b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/Contents.json @@ -1,23 +1,15 @@ { "images" : [ { - "filename" : "IT Finanziato dall'Unione europea.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "IT Finanziato dall'Unione europea 1.svg", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "IT Finanziato dall'Unione europea 2.svg", - "idiom" : "universal", - "scale" : "3x" + "filename" : "eu_next_logo.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 1.svg b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 1.svg deleted file mode 100644 index 244bce156e3..00000000000 --- a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 1.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 2.svg b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 2.svg deleted file mode 100644 index 244bce156e3..00000000000 --- a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea 2.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea.svg b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea.svg deleted file mode 100644 index 244bce156e3..00000000000 --- a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/IT Finanziato dall'Unione europea.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/eu_next_logo.svg b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/eu_next_logo.svg new file mode 100644 index 00000000000..fbe13d7d535 --- /dev/null +++ b/ios/ItaliaApp/Images.xcassets/euNextLogo.imageset/eu_next_logo.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/Contents.json b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/Contents.json index 1a9c9cf6045..9b0051f53d9 100644 --- a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/Contents.json +++ b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/Contents.json @@ -1,23 +1,15 @@ { "images" : [ { - "filename" : "logo_ConsiglioMInistri.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "logo ConsiglioMInistri@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "logo ConsiglioMInistri@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "logo_consiglio_ministri.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@2x.png b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@2x.png deleted file mode 100644 index fea6caf2353..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@2x.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@3x.png b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@3x.png deleted file mode 100644 index 722aff5ec14..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo ConsiglioMInistri@3x.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_ConsiglioMInistri.png b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_ConsiglioMInistri.png deleted file mode 100644 index 1d3df27ef4a..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_ConsiglioMInistri.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_consiglio_ministri.svg b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_consiglio_ministri.svg new file mode 100644 index 00000000000..6b00504a44b --- /dev/null +++ b/ios/ItaliaApp/Images.xcassets/logo_consiglio_ministri.imageset/logo_consiglio_ministri.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/Contents.json b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/Contents.json index 8217da938e6..b96ee49db12 100644 --- a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/Contents.json +++ b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/Contents.json @@ -1,23 +1,15 @@ { "images" : [ { - "filename" : "io-app-logo.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "io-app-logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "io-app-logo@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "logo_io.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo.png b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo.png deleted file mode 100644 index 2734d0b64d5..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@2x.png b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@2x.png deleted file mode 100644 index c9364152e28..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@2x.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@3x.png b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@3x.png deleted file mode 100644 index 333146f2cc5..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/io-app-logo@3x.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_io.imageset/logo_io.svg b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/logo_io.svg new file mode 100644 index 00000000000..cded5bf083e --- /dev/null +++ b/ios/ItaliaApp/Images.xcassets/logo_io.imageset/logo_io.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/Contents.json b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/Contents.json index 4d1d95eeb61..b499e9e931e 100644 --- a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/Contents.json +++ b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/Contents.json @@ -1,23 +1,15 @@ { "images" : [ { - "filename" : "logo_pagopa.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "pagopa - logo - darkbg - grayscale - transparency@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "pagopa - logo - darkbg - grayscale - transparency@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "logo_pagopa.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.png b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.png deleted file mode 100644 index 65fcac8645d..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.svg b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.svg new file mode 100644 index 00000000000..744feee4f31 --- /dev/null +++ b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/logo_pagopa.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@2x.png b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@2x.png deleted file mode 100644 index 15ffa878a1b..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@2x.png and /dev/null differ diff --git a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@3x.png b/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@3x.png deleted file mode 100644 index 26ae73dd950..00000000000 Binary files a/ios/ItaliaApp/Images.xcassets/logo_pagopa.imageset/pagopa - logo - darkbg - grayscale - transparency@3x.png and /dev/null differ diff --git a/ios/ItaliaApp/Info.plist b/ios/ItaliaApp/Info.plist index fb5ab19ec9d..29a5eb5a6b2 100644 --- a/ios/ItaliaApp/Info.plist +++ b/ios/ItaliaApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.53.0 + 2.58.0 CFBundleSignature ???? CFBundleURLTypes @@ -110,6 +110,18 @@ RobotoMono-RegularItalic.ttf ReadexPro-Regular.ttf DMMono-Medium.ttf + TitilliumSansPro-Black.otf + TitilliumSansPro-BlackItalic.otf + TitilliumSansPro-Bold.otf + TitilliumSansPro-BoldItalic.otf + TitilliumSansPro-Italic.otf + TitilliumSansPro-Light.otf + TitilliumSansPro-LightItalic.otf + TitilliumSansPro-Regular.otf + TitilliumSansPro-Semibold.otf + TitilliumSansPro-SemiboldItalic.otf + TitilliumSansPro-Thin.otf + TitilliumSansPro-ThinItalic.otf UIBackgroundModes diff --git a/ios/ItaliaApp/LaunchScreen.storyboard b/ios/ItaliaApp/LaunchScreen.storyboard index fe52cff7812..a4d279bc190 100644 --- a/ios/ItaliaApp/LaunchScreen.storyboard +++ b/ios/ItaliaApp/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - + - + @@ -22,69 +22,50 @@ - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + + + - + + + + - + - - - - - - + + - - - + + - + - + + + @@ -95,10 +76,10 @@ - - + + - + diff --git a/ios/ItaliaApp/de.lproj/InfoPlist.strings b/ios/ItaliaApp/de.lproj/InfoPlist.strings new file mode 100644 index 00000000000..80f19a40455 --- /dev/null +++ b/ios/ItaliaApp/de.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +"NSCameraUsageDescription" = "Du kannst QR-Codes lesen und pagoPA-Mitteilungen bezahlen."; +"NSCalendarsUsageDescription" = "Du kannst Fälligkeiten in deinem Kalender eintragen und eine Benachrichtigung einstellen."; +"NSContactsUsageDescription" = "IO benötigt Zugriff auf deine Kontakte, damit du sie als Teilnehmer zu Terminen hinzufügen kannst."; +"NSFaceIDUsageDescription" = "IO benötigt Zugriff auf Face ID, um eine schnellere Authentifizierung zu ermöglichen."; +"NSPhotoLibraryUsageDescription" = "Du kannst Gutscheine und Zertifikate speichern und Screenshots und Zahlungsmitteilungen hochladen."; +"NSMicrophoneUsageDescription" = "IO benötigt Zugriff auf das Mikrofon, wenn du eine Sprachnachricht senden möchtest."; +"NSPhotoLibraryAddUsageDescription" = "Du kannst Bilder aus der App auf deinem Gerät speichern."; \ No newline at end of file diff --git a/ios/ItaliaApp/de.lproj/Localizable.strings b/ios/ItaliaApp/de.lproj/Localizable.strings new file mode 100755 index 00000000000..43bb22c1062 --- /dev/null +++ b/ios/ItaliaApp/de.lproj/Localizable.strings @@ -0,0 +1,2 @@ +"ALERT_DEVICE_ROOTED_TITLE" = "Gerät mit Jailbreak"; +"ALERT_DEVICE_ROOTED_DESC" = "Dieses Gerät ist gejailbreakt, du kannst diese App aus Sicherheitsgründen nicht verwenden"; diff --git a/ios/ItaliaAppTests/Info.plist b/ios/ItaliaAppTests/Info.plist index 23908b163b5..d4c8a536487 100644 --- a/ios/ItaliaAppTests/Info.plist +++ b/ios/ItaliaAppTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2.53.0 + 2.58.0 CFBundleSignature ???? CFBundleVersion diff --git a/ios/Podfile b/ios/Podfile index 74b7bbb04aa..e90b1202f53 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -23,8 +23,9 @@ target 'ItaliaApp' do pod 'ReactNativeART', :podspec => '../node_modules/@react-native-community/art/ReactNativeART.podspec' use_react_native!( :path => config[:reactNativePath], - # to enable hermes on iOS, change `false` to `true` and then install pods - :production => production, + # Hermes is now enabled by default. Disable by setting this flag to false. + # Upcoming versions of React Native may rely on get_default_flags(), but + # we make it explicit here to aid in the React Native upgrade process. :hermes_enabled => true, :fabric_enabled => false, # An absolute path to your application root. @@ -44,10 +45,26 @@ target 'ItaliaApp' do end post_install do |installer| - react_native_post_install(installer) + react_native_post_install( + installer, + # Set `mac_catalyst_enabled` to `true` in order to apply patches + # necessary for Mac Catalyst builds + :mac_catalyst_enabled => false + ) + __apply_Xcode_12_5_M1_post_install_workaround(installer) installer.pods_project.targets.each do |target| target.build_configurations.each do |config| + if target.name == 'Flipper' + file_path = 'Pods/Flipper/xplat/Flipper/FlipperTransportTypes.h' + contents = File.read(file_path) + unless contents.include?('#include ') + File.open(file_path, 'w') do |file| + file.puts('#include ') + file.puts(contents) + end + end + end # This is needed in order to build the project on Apple silicon. # Beware that after this modification you need Rosetta to run the app # on the simulator. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3e3269eef8a..7e61b148698 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,17 +1,18 @@ PODS: + - Alamofire (5.8.1) - boost (1.76.0) - BVLinearGradient (2.5.6): - React - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - - FBLazyVector (0.69.9) - - FBReactNativeSpec (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTRequired (= 0.69.9) - - RCTTypeSafety (= 0.69.9) - - React-Core (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) + - FBLazyVector (0.70.15) + - FBReactNativeSpec (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTRequired (= 0.70.15) + - RCTTypeSafety (= 0.70.15) + - React-Core (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) - Flipper (0.154.0): - Flipper-Folly (~> 2.6) - Flipper-Boost-iOSX (1.76.0.1.11) @@ -74,15 +75,15 @@ PODS: - FlipperKit/FlipperKitNetworkPlugin - fmt (6.2.1) - glog (0.3.5) - - GoogleDataTransport (9.1.2): - - GoogleUtilities/Environment (~> 7.2) - - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.3.0): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (2.5.0): + - GoogleMLKit/BarcodeScanning (4.0.0): - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 1.6.0) - - GoogleMLKit/MLKitCore (2.5.0): - - MLKitCommon (~> 5.0.0) + - MLKitBarcodeScanning (~> 3.0.0) + - GoogleMLKit/MLKitCore (4.0.0): + - MLKitCommon (~> 9.0.0) - GoogleToolboxForMac/DebugUtils (2.3.2): - GoogleToolboxForMac/Defines (= 2.3.2) - GoogleToolboxForMac/Defines (2.3.2) @@ -95,277 +96,288 @@ PODS: - GoogleToolboxForMac/Defines (= 2.3.2) - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/Environment (7.7.0): + - GoogleUtilities/Environment (7.12.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Logger (7.12.0): - GoogleUtilities/Environment - - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/UserDefaults (7.12.0): - GoogleUtilities/Logger - GoogleUtilitiesComponents (1.1.0): - GoogleUtilities/Logger - - GTMSessionFetcher/Core (1.7.0) - - hermes-engine (0.69.9) + - GTMSessionFetcher/Core (2.3.0) + - hermes-engine (0.70.15) - jail-monkey (2.3.2): - React - libevent (2.1.12) - - Mixpanel (3.6.5) - - MLImage (1.0.0-beta2) - - MLKitBarcodeScanning (1.6.0): - - MLKitCommon (~> 5.0) - - MLKitVision (~> 3.0) - - MLKitCommon (5.0.0): + - Mixpanel-swift (4.2.0): + - Mixpanel-swift/Complete (= 4.2.0) + - Mixpanel-swift/Complete (4.2.0) + - MixpanelReactNative (2.4.1): + - Mixpanel-swift (= 4.2.0) + - React-Core + - MLImage (1.0.0-beta4) + - MLKitBarcodeScanning (3.0.0): + - MLKitCommon (~> 9.0) + - MLKitVision (~> 5.0) + - MLKitCommon (9.0.0): - GoogleDataTransport (~> 9.0) - GoogleToolboxForMac/Logger (~> 2.1) - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - GoogleUtilities/UserDefaults (~> 7.0) - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (~> 1.1) - - Protobuf (~> 3.12) - - MLKitVision (3.0.0): + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - MLKitVision (5.0.0): - GoogleToolboxForMac/Logger (~> 2.1) - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - GTMSessionFetcher/Core (~> 1.1) - - MLImage (= 1.0.0-beta2) - - MLKitCommon (~> 5.0) - - Protobuf (~> 3.12) - - nanopb (2.30908.0): - - nanopb/decode (= 2.30908.0) - - nanopb/encode (= 2.30908.0) - - nanopb/decode (2.30908.0) - - nanopb/encode (2.30908.0) + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - MLImage (= 1.0.0-beta4) + - MLKitCommon (~> 9.0) + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - OpenSSL-Universal (1.1.1100) - - pagopa-io-react-native-crypto (0.2.1): + - pagopa-io-react-native-crypto (0.3.0): + - React-Core + - pagopa-io-react-native-http-client (0.1.3): + - Alamofire - React-Core - - pagopa-io-react-native-login-utils (0.2.2): + - pagopa-io-react-native-login-utils (1.0.0): - React-Core - - PromisesObjC (2.0.0) - - Protobuf (3.19.1) - - RCT-Folly (2021.06.28.00-v2): + - pagopa-react-native-zendesk (0.3.29): + - React-Core + - ZendeskAnswerBotSDK + - ZendeskChatSDK + - ZendeskMessagingAPISDK + - ZendeskSupportSDK + - PromisesObjC (2.3.1) + - RCT-Folly (2021.07.22.00): - boost - DoubleConversion - fmt (~> 6.2.1) - glog - - RCT-Folly/Default (= 2021.06.28.00-v2) - - RCT-Folly/Default (2021.06.28.00-v2): + - RCT-Folly/Default (= 2021.07.22.00) + - RCT-Folly/Default (2021.07.22.00): - boost - DoubleConversion - fmt (~> 6.2.1) - glog - - RCT-Folly/Futures (2021.06.28.00-v2): + - RCT-Folly/Futures (2021.07.22.00): - boost - DoubleConversion - fmt (~> 6.2.1) - glog - libevent - - RCTRequired (0.69.9) - - RCTTypeSafety (0.69.9): - - FBLazyVector (= 0.69.9) - - RCTRequired (= 0.69.9) - - React-Core (= 0.69.9) - - React (0.69.9): - - React-Core (= 0.69.9) - - React-Core/DevSupport (= 0.69.9) - - React-Core/RCTWebSocket (= 0.69.9) - - React-RCTActionSheet (= 0.69.9) - - React-RCTAnimation (= 0.69.9) - - React-RCTBlob (= 0.69.9) - - React-RCTImage (= 0.69.9) - - React-RCTLinking (= 0.69.9) - - React-RCTNetwork (= 0.69.9) - - React-RCTSettings (= 0.69.9) - - React-RCTText (= 0.69.9) - - React-RCTVibration (= 0.69.9) - - React-bridging (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - React-jsi (= 0.69.9) - - React-callinvoker (0.69.9) - - React-Codegen (0.69.9): - - FBReactNativeSpec (= 0.69.9) - - RCT-Folly (= 2021.06.28.00-v2) - - RCTRequired (= 0.69.9) - - RCTTypeSafety (= 0.69.9) - - React-Core (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-Core (0.69.9): + - RCTRequired (0.70.15) + - RCTTypeSafety (0.70.15): + - FBLazyVector (= 0.70.15) + - RCTRequired (= 0.70.15) + - React-Core (= 0.70.15) + - React (0.70.15): + - React-Core (= 0.70.15) + - React-Core/DevSupport (= 0.70.15) + - React-Core/RCTWebSocket (= 0.70.15) + - React-RCTActionSheet (= 0.70.15) + - React-RCTAnimation (= 0.70.15) + - React-RCTBlob (= 0.70.15) + - React-RCTImage (= 0.70.15) + - React-RCTLinking (= 0.70.15) + - React-RCTNetwork (= 0.70.15) + - React-RCTSettings (= 0.70.15) + - React-RCTText (= 0.70.15) + - React-RCTVibration (= 0.70.15) + - React-bridging (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - React-jsi (= 0.70.15) + - React-callinvoker (0.70.15) + - React-Codegen (0.70.15): + - FBReactNativeSpec (= 0.70.15) + - RCT-Folly (= 2021.07.22.00) + - RCTRequired (= 0.70.15) + - RCTTypeSafety (= 0.70.15) + - React-Core (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-Core (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-Core/Default (= 0.69.9) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.70.15) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/CoreModulesHeaders (0.69.9): + - React-Core/CoreModulesHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/Default (0.69.9): + - React-Core/Default (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/DevSupport (0.69.9): + - React-Core/DevSupport (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-Core/Default (= 0.69.9) - - React-Core/RCTWebSocket (= 0.69.9) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-jsinspector (= 0.69.9) - - React-perflogger (= 0.69.9) + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.70.15) + - React-Core/RCTWebSocket (= 0.70.15) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-jsinspector (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTActionSheetHeaders (0.69.9): + - React-Core/RCTActionSheetHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTAnimationHeaders (0.69.9): + - React-Core/RCTAnimationHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTBlobHeaders (0.69.9): + - React-Core/RCTBlobHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTImageHeaders (0.69.9): + - React-Core/RCTImageHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTLinkingHeaders (0.69.9): + - React-Core/RCTLinkingHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTNetworkHeaders (0.69.9): + - React-Core/RCTNetworkHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTSettingsHeaders (0.69.9): + - React-Core/RCTSettingsHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTTextHeaders (0.69.9): + - React-Core/RCTTextHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTVibrationHeaders (0.69.9): + - React-Core/RCTVibrationHeaders (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-Core/RCTWebSocket (0.69.9): + - React-Core/RCTWebSocket (0.70.15): - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-Core/Default (= 0.69.9) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-perflogger (= 0.69.9) + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.70.15) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-perflogger (= 0.70.15) - Yoga - - React-CoreModules (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTTypeSafety (= 0.69.9) - - React-Codegen (= 0.69.9) - - React-Core/CoreModulesHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - React-RCTImage (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-cxxreact (0.69.9): + - React-CoreModules (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.70.15) + - React-Codegen (= 0.70.15) + - React-Core/CoreModulesHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - React-RCTImage (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-cxxreact (0.70.15): - boost (= 1.76.0) - DoubleConversion - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-callinvoker (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsinspector (= 0.69.9) - - React-logger (= 0.69.9) - - React-perflogger (= 0.69.9) - - React-runtimeexecutor (= 0.69.9) - - React-hermes (0.69.9): + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsinspector (= 0.70.15) + - React-logger (= 0.70.15) + - React-perflogger (= 0.70.15) + - React-runtimeexecutor (= 0.70.15) + - React-hermes (0.70.15): - DoubleConversion - glog - hermes-engine - - RCT-Folly (= 2021.06.28.00-v2) - - RCT-Folly/Futures (= 2021.06.28.00-v2) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-jsiexecutor (= 0.69.9) - - React-jsinspector (= 0.69.9) - - React-perflogger (= 0.69.9) - - React-jsi (0.69.9): + - RCT-Folly (= 2021.07.22.00) + - RCT-Folly/Futures (= 2021.07.22.00) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-jsiexecutor (= 0.70.15) + - React-jsinspector (= 0.70.15) + - React-perflogger (= 0.70.15) + - React-jsi (0.70.15): - boost (= 1.76.0) - DoubleConversion - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-jsi/Default (= 0.69.9) - - React-jsi/Default (0.69.9): + - RCT-Folly (= 2021.07.22.00) + - React-jsi/Default (= 0.70.15) + - React-jsi/Default (0.70.15): - boost (= 1.76.0) - DoubleConversion - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-jsiexecutor (0.69.9): + - RCT-Folly (= 2021.07.22.00) + - React-jsiexecutor (0.70.15): - DoubleConversion - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-perflogger (= 0.69.9) - - React-jsinspector (0.69.9) - - React-logger (0.69.9): + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-perflogger (= 0.70.15) + - React-jsinspector (0.70.15) + - React-logger (0.70.15): - glog - react-native-background-timer (2.1.1): - React @@ -379,7 +391,7 @@ PODS: - React-Core - react-native-cookies (6.2.1): - React-Core - - react-native-document-picker (9.0.1): + - react-native-document-picker (9.1.1): - React-Core - react-native-fingerprint-scanner (6.0.0): - React @@ -389,9 +401,8 @@ PODS: - React-Core - react-native-image-picker (4.10.3): - React-Core - - react-native-mixpanel (1.2.0): - - Mixpanel (~> 3.6.0) - - React + - react-native-pager-view (6.2.3): + - React-Core - react-native-pdf (6.4.0): - React-Core - react-native-pdf-thumbnail (1.2.1): @@ -410,72 +421,72 @@ PODS: - React - react-native-webview (11.26.1): - React-Core - - React-perflogger (0.69.9) - - React-RCTActionSheet (0.69.9): - - React-Core/RCTActionSheetHeaders (= 0.69.9) - - React-RCTAnimation (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTTypeSafety (= 0.69.9) - - React-Codegen (= 0.69.9) - - React-Core/RCTAnimationHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTBlob (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - React-Codegen (= 0.69.9) - - React-Core/RCTBlobHeaders (= 0.69.9) - - React-Core/RCTWebSocket (= 0.69.9) - - React-jsi (= 0.69.9) - - React-RCTNetwork (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTImage (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTTypeSafety (= 0.69.9) - - React-Codegen (= 0.69.9) - - React-Core/RCTImageHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - React-RCTNetwork (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTLinking (0.69.9): - - React-Codegen (= 0.69.9) - - React-Core/RCTLinkingHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTNetwork (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTTypeSafety (= 0.69.9) - - React-Codegen (= 0.69.9) - - React-Core/RCTNetworkHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTSettings (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - RCTTypeSafety (= 0.69.9) - - React-Codegen (= 0.69.9) - - React-Core/RCTSettingsHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-RCTText (0.69.9): - - React-Core/RCTTextHeaders (= 0.69.9) - - React-RCTVibration (0.69.9): - - RCT-Folly (= 2021.06.28.00-v2) - - React-Codegen (= 0.69.9) - - React-Core/RCTVibrationHeaders (= 0.69.9) - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (= 0.69.9) - - React-runtimeexecutor (0.69.9): - - React-jsi (= 0.69.9) - - ReactCommon/turbomodule/core (0.69.9): + - React-perflogger (0.70.15) + - React-RCTActionSheet (0.70.15): + - React-Core/RCTActionSheetHeaders (= 0.70.15) + - React-RCTAnimation (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.70.15) + - React-Codegen (= 0.70.15) + - React-Core/RCTAnimationHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTBlob (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.70.15) + - React-Core/RCTBlobHeaders (= 0.70.15) + - React-Core/RCTWebSocket (= 0.70.15) + - React-jsi (= 0.70.15) + - React-RCTNetwork (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTImage (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.70.15) + - React-Codegen (= 0.70.15) + - React-Core/RCTImageHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - React-RCTNetwork (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTLinking (0.70.15): + - React-Codegen (= 0.70.15) + - React-Core/RCTLinkingHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTNetwork (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.70.15) + - React-Codegen (= 0.70.15) + - React-Core/RCTNetworkHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTSettings (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.70.15) + - React-Codegen (= 0.70.15) + - React-Core/RCTSettingsHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-RCTText (0.70.15): + - React-Core/RCTTextHeaders (= 0.70.15) + - React-RCTVibration (0.70.15): + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.70.15) + - React-Core/RCTVibrationHeaders (= 0.70.15) + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (= 0.70.15) + - React-runtimeexecutor (0.70.15): + - React-jsi (= 0.70.15) + - ReactCommon/turbomodule/core (0.70.15): - DoubleConversion - glog - - RCT-Folly (= 2021.06.28.00-v2) - - React-bridging (= 0.69.9) - - React-callinvoker (= 0.69.9) - - React-Core (= 0.69.9) - - React-cxxreact (= 0.69.9) - - React-jsi (= 0.69.9) - - React-logger (= 0.69.9) - - React-perflogger (= 0.69.9) + - RCT-Folly (= 2021.07.22.00) + - React-bridging (= 0.70.15) + - React-callinvoker (= 0.70.15) + - React-Core (= 0.70.15) + - React-cxxreact (= 0.70.15) + - React-jsi (= 0.70.15) + - React-logger (= 0.70.15) + - React-perflogger (= 0.70.15) - ReactNativeART (1.2.0): - React - ReactNativeExceptionHandler (2.10.8): @@ -486,17 +497,13 @@ PODS: - React-Core - RNCClipboard (1.10.0): - React-Core - - RNCPicker (2.4.1): - - React-Core - RNCPushNotificationIOS (1.8.0): - React-Core - - RNDateTimePicker (3.5.2): - - React-Core - RNDeviceInfo (10.8.0): - React-Core - RNFS (2.18.0): - React - - RNGestureHandler (2.12.0): + - RNGestureHandler (2.15.0): - React-Core - RNI18n (2.0.15): - React @@ -536,21 +543,16 @@ PODS: - React-RCTText - ReactCommon/turbomodule/core - Yoga - - RNScreens (2.18.1): + - RNScreens (3.30.1): - React-Core + - React-RCTImage - RNSha256 (1.2.3): - React - RNShare (7.3.9): - React-Core - - RNSVG (12.3.0): + - RNSVG (15.1.0): - React-Core - - RNZendeskChat (0.3.23): - - React - - ZendeskAnswerBotSDK (~> 3.0.0) - - ZendeskChatSDK (~> 3.0.0) - - ZendeskMessagingAPISDK (~> 4.0.0) - - ZendeskSupportSDK (~> 6.0.0) - - SocketRocket (0.6.0) + - SocketRocket (0.6.1) - vision-camera-code-scanner (0.2.0): - GoogleMLKit/BarcodeScanning - React-Core @@ -561,28 +563,28 @@ PODS: - Yoga (1.14.0) - YogaKit (1.18.1): - Yoga (~> 1.14) - - ZendeskAnswerBotProvidersSDK (3.0.0): - - ZendeskSupportProvidersSDK (= 6.0.0) - - ZendeskAnswerBotSDK (3.0.0): - - ZendeskAnswerBotProvidersSDK (= 3.0.0) - - ZendeskMessagingSDK (= 4.0.0) - - ZendeskChatProvidersSDK (3.0.0) - - ZendeskChatSDK (3.0.0): - - ZendeskChatProvidersSDK (= 3.0.0) - - ZendeskMessagingSDK (= 4.0.0) - - ZendeskCommonUISDK (7.0.0) - - ZendeskCoreSDK (3.0.0) - - ZendeskMessagingAPISDK (4.0.0): - - ZendeskSDKConfigurationsSDK (= 2.0.0) - - ZendeskMessagingSDK (4.0.0): - - ZendeskCommonUISDK (= 7.0.0) - - ZendeskMessagingAPISDK (= 4.0.0) - - ZendeskSDKConfigurationsSDK (2.0.0) - - ZendeskSupportProvidersSDK (6.0.0): - - ZendeskCoreSDK (= 3.0.0) - - ZendeskSupportSDK (6.0.0): - - ZendeskMessagingSDK (= 4.0.0) - - ZendeskSupportProvidersSDK (= 6.0.0) + - ZendeskAnswerBotProvidersSDK (5.0.0): + - ZendeskSupportProvidersSDK (~> 8.0.0) + - ZendeskAnswerBotSDK (5.0.0): + - ZendeskAnswerBotProvidersSDK (~> 5.0.0) + - ZendeskMessagingSDK (~> 6.0.0) + - ZendeskChatProvidersSDK (5.0.0) + - ZendeskChatSDK (5.0.0): + - ZendeskChatProvidersSDK (~> 5.0.0) + - ZendeskMessagingSDK (~> 6.0.0) + - ZendeskCommonUISDK (9.0.0) + - ZendeskCoreSDK (5.0.0) + - ZendeskMessagingAPISDK (6.0.0): + - ZendeskSDKConfigurationsSDK (~> 4.0.0) + - ZendeskMessagingSDK (6.0.0): + - ZendeskCommonUISDK (~> 9.0.0) + - ZendeskMessagingAPISDK (~> 6.0.0) + - ZendeskSDKConfigurationsSDK (4.0.0) + - ZendeskSupportProvidersSDK (8.0.0): + - ZendeskCoreSDK (~> 5.0.0) + - ZendeskSupportSDK (8.0.0): + - ZendeskMessagingSDK (~> 6.0.0) + - ZendeskSupportProvidersSDK (~> 8.0.0) - ZXingObjC (3.6.5): - ZXingObjC/All (= 3.6.5) - ZXingObjC/All (3.6.5) @@ -618,9 +620,12 @@ DEPENDENCIES: - hermes-engine (from `../node_modules/react-native/sdks/hermes/hermes-engine.podspec`) - jail-monkey (from `../node_modules/jail-monkey`) - libevent (~> 2.1.12) + - MixpanelReactNative (from `../node_modules/mixpanel-react-native`) - OpenSSL-Universal (= 1.1.1100) - "pagopa-io-react-native-crypto (from `../node_modules/@pagopa/io-react-native-crypto`)" + - "pagopa-io-react-native-http-client (from `../node_modules/@pagopa/io-react-native-http-client`)" - "pagopa-io-react-native-login-utils (from `../node_modules/@pagopa/io-react-native-login-utils`)" + - "pagopa-react-native-zendesk (from `../node_modules/@pagopa/io-react-native-zendesk`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) @@ -647,7 +652,7 @@ DEPENDENCIES: - react-native-flipper (from `../node_modules/react-native-flipper`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - - react-native-mixpanel (from `../node_modules/react-native-mixpanel`) + - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-pdf (from `../node_modules/react-native-pdf`) - react-native-pdf-thumbnail (from `../node_modules/react-native-pdf-thumbnail`) - react-native-render-html (from `../node_modules/react-native-render-html`) @@ -674,9 +679,7 @@ DEPENDENCIES: - RNCalendarEvents (from `../node_modules/react-native-calendar-events`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) @@ -690,13 +693,13 @@ DEPENDENCIES: - RNSha256 (from `../node_modules/react-native-sha256`) - RNShare (from `../node_modules/react-native-share`) - RNSVG (from `../node_modules/react-native-svg`) - - RNZendeskChat (from `../node_modules/io-react-native-zendesk`) - vision-camera-code-scanner (from `../node_modules/vision-camera-code-scanner`) - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: + - Alamofire - CocoaAsyncSocket - Flipper - Flipper-Boost-iOSX @@ -715,7 +718,7 @@ SPEC REPOS: - GoogleUtilitiesComponents - GTMSessionFetcher - libevent - - Mixpanel + - Mixpanel-swift - MLImage - MLKitBarcodeScanning - MLKitCommon @@ -723,7 +726,6 @@ SPEC REPOS: - nanopb - OpenSSL-Universal - PromisesObjC - - Protobuf - SocketRocket - YogaKit - ZendeskAnswerBotProvidersSDK @@ -756,10 +758,16 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/sdks/hermes/hermes-engine.podspec" jail-monkey: :path: "../node_modules/jail-monkey" + MixpanelReactNative: + :path: "../node_modules/mixpanel-react-native" pagopa-io-react-native-crypto: :path: "../node_modules/@pagopa/io-react-native-crypto" + pagopa-io-react-native-http-client: + :path: "../node_modules/@pagopa/io-react-native-http-client" pagopa-io-react-native-login-utils: :path: "../node_modules/@pagopa/io-react-native-login-utils" + pagopa-react-native-zendesk: + :path: "../node_modules/@pagopa/io-react-native-zendesk" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTRequired: @@ -810,8 +818,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-get-random-values" react-native-image-picker: :path: "../node_modules/react-native-image-picker" - react-native-mixpanel: - :path: "../node_modules/react-native-mixpanel" + react-native-pager-view: + :path: "../node_modules/react-native-pager-view" react-native-pdf: :path: "../node_modules/react-native-pdf" react-native-pdf-thumbnail: @@ -864,12 +872,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" - RNCPicker: - :path: "../node_modules/@react-native-picker/picker" RNCPushNotificationIOS: :path: "../node_modules/@react-native-community/push-notification-ios" - RNDateTimePicker: - :path: "../node_modules/@react-native-community/datetimepicker" RNDeviceInfo: :path: "../node_modules/react-native-device-info" RNFS: @@ -896,8 +900,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-share" RNSVG: :path: "../node_modules/react-native-svg" - RNZendeskChat: - :path: "../node_modules/io-react-native-zendesk" vision-camera-code-scanner: :path: "../node_modules/vision-camera-code-scanner" VisionCamera: @@ -906,12 +908,13 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: a7c83b31436843459a1961bfd74b96033dc77234 + Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 + boost: 9fa78656d705f55b1220151d997e57e2a3f2cde0 BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 - FBLazyVector: d3c1d2923b1009f4e709e8f1b793dedf5b323454 - FBReactNativeSpec: d460df7d796ed4979c6cd4e092145b63eb28b5bb + FBLazyVector: 9cf707e46f9bd90816b7c91b2c1c8b8a2f549527 + FBReactNativeSpec: 5ce1ea97a4309ded19af6c21f13f63ee3cabfed2 Flipper: 53851f5b89559bb6e251572589dc166d1f8d6e2e Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 @@ -922,53 +925,55 @@ SPEC CHECKSUMS: Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541 FlipperKit: 51cf8b6f5b0931e251c57d4d60a15a1c2ba546aa fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a - GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 - GoogleMLKit: 0d7f5aa2f8a2f2ea9d849a05abdbe80974b0ec83 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe + GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 - hermes-engine: f648f65e30dd2ef478c08024259c881f0a1522bf + GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + hermes-engine: 2592781da1571e4375dfd897f9462638c2d0ceb9 jail-monkey: d7c5048b2336f22ee9c9e0efa145f1f917338ea9 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - Mixpanel: de8296d8568806ab17bfced0f823c908b808e1e0 - MLImage: a454f9f8ecfd537783a12f9488f5be1a68820829 - MLKitBarcodeScanning: 867463093fe27a31260898e239d5d8305be29b28 - MLKitCommon: 3bc17c6f7d25ce3660f030350b46ae7ec9ebca6e - MLKitVision: e87dc3f2e456a6ab32361ebd985e078dd2746143 - nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + Mixpanel-swift: e5dd85295923e6a875acf17ccbab8d2ecb10ea65 + MixpanelReactNative: 0101b8828c2f335c128850e71ab7d3b7adde089a + MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b + MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 + MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 + MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - pagopa-io-react-native-crypto: 644fece16966f2e1ea1f872344ee5a3c6c8761a1 - pagopa-io-react-native-login-utils: 51a58dc0e5fe3cba461759b9e98e795fc22e17c8 - PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 - Protobuf: 3724efa50cb2846d7ccebc8691c574e85fd74471 - RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a - RCTRequired: fe80e9f71dd15939e5399dd94d0aa0e5e069d592 - RCTTypeSafety: 4c802f7c4b9e7c55470e7fc56d50385cbfcce89b - React: efe0b6d0b7401a208d2d1bf8320c0b6a0dcd593b - React-bridging: 11a324da43d8467cfe528ccff0e00ab43bdf1cf4 - React-callinvoker: d4d34002df449053784f1131a6382e526d172395 - React-Codegen: 63eb91553568558cbd6fb2f336c3ff2fea66b37a - React-Core: 2875b1749729d07ef7cacef36e8b1381f27cc86e - React-CoreModules: 8b6665f9b128b875660d75a7144122c742ad4af9 - React-cxxreact: 76d426551a4d1d6f623ef8f87a26690d5a6be93e - React-hermes: 9adb1d978e6d6b39fb6f4a4b340e50bf1fcfdf08 - React-jsi: 47b65f4f789f198004c47e5b6ceaae95ea3f3659 - React-jsiexecutor: 3b506d7fc50275bf44f8fd5624f23eaedd78bf78 - React-jsinspector: 5b92a5a30e02e1a21802f293cc71e05d887c7378 - React-logger: 96d904c5bc87c2660d48e6a36fb2edf65f9cfb11 + pagopa-io-react-native-crypto: 6aa9f33e4bf64ef420ad97c720c1ad0f876cd470 + pagopa-io-react-native-http-client: 14b1ed3cd090108f9cb324f951811fe556b81e51 + pagopa-io-react-native-login-utils: 442a5e2ab8db2c476fed2cff6d7ad16388ff1f21 + pagopa-react-native-zendesk: e4a63ee0745a567b641110f7ff78e457086ab7a3 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda + RCTRequired: 2a96ea90ffddd10cc43115bd93803692e09b5d9a + RCTTypeSafety: 02c99baddcf0b3393bf58e6d9b792e83a37716b4 + React: 45e3210df90d25ec6da7fc286943b377b63a92ec + React-bridging: e3a18265bbd59003562e29429985e0923a5b6286 + React-callinvoker: 344ff205a470c3c99b4daf0a2dff9bc29045d6c5 + React-Codegen: 2b1765b0e1a38b8b3601178ca27c1e9216e81632 + React-Core: 93efb81ef85fafee7f83f7ef6ecf546b2e1ee2c0 + React-CoreModules: 4eb535b1650b718cb3680767c1b9a1cacf649cbc + React-cxxreact: 283248db3101de28d6cf0fe438a2dc95537ee472 + React-hermes: 5439b771de0b04930c97888cc4c28852aa37389c + React-jsi: 560bdf0bc36d5c137ac962c0eb4b60b50c304d77 + React-jsiexecutor: d2eebcd5a432f90be3baa5d1309f47d05478ea61 + React-jsinspector: bfedded1f4f562d29c2d4a8bb795c9a150a739e4 + React-logger: 31f198387a04172be49fe38e41a082560a81aeeb react-native-background-timer: 1b6e6b4e10f1b74c367a1fdc3c72b67c619b222b react-native-blob-util: a5d3561045ed98cfb2fb80cbbff600fae0e8edee react-native-cameraroll: 2f08db1ecc9b73dbc01f89335d6d5179fac2894c react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14 react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c - react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156 + react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452 react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe react-native-flipper: 97d537d855e0e7f6ac26a065e01bf1aecc8ba41c react-native-get-random-values: 237bffb1c7e05fb142092681531810a29ba53015 react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695 - react-native-mixpanel: d644efe1ca33d2646d5cba29e24a13ebc9b37209 + react-native-pager-view: c29d484f19c49ff19525a94105e4ab2c4d4ae273 react-native-pdf: a6a5a3f0bdf340eb2eed6c96034424d2cc3f84b0 react-native-pdf-thumbnail: a042fffdab7a49f0f9df0e11da0a90beebfd4241 react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c @@ -978,58 +983,55 @@ SPEC CHECKSUMS: react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1 - React-perflogger: f6f4b3c63629ead2e8a5a22eaedd06368c31617a - React-RCTActionSheet: 5e95058e99c53313d778d96b7f37697ce3a9418e - React-RCTAnimation: c4f86d756d8398c674f61d00075993a368d83480 - React-RCTBlob: c74cb1350831866b3b23c2379ccc3a5909593bc5 - React-RCTImage: cc72e4092e08c7070d1dce7704fbdcdfc9e0a721 - React-RCTLinking: dca79b9df468980462a399a630156f5a5fc0b5bc - React-RCTNetwork: 0dfb918fd237b6fa4e3820769c57a6a0ad61f934 - React-RCTSettings: a9fb6736139ddf8e7d11842c8a948c47c1ae603d - React-RCTText: 87456d45e8bcc0c831b7c7fcfcfd860a54f54a79 - React-RCTVibration: ea899478e6f10ee526f476f769ab33211be2addd - React-runtimeexecutor: df1518d092e8c74cafddc56690d1ac386ec24d7a - ReactCommon: fac40473e2c4117522384027ab33ad0cb6717dc5 + React-perflogger: 010e98d3335e5185a8f7496babca50d82a042e84 + React-RCTActionSheet: 0f585d684b540a5bbfc62b0a1fbc5292cff2aefc + React-RCTAnimation: eb0e5b020333f9cc652d85f27a47086fbf56fffd + React-RCTBlob: 4af18ad2a64515c3ede9b829e8532f1508e00894 + React-RCTImage: 08787efa5378ad0e7344943eed1b898619cf956a + React-RCTLinking: ea7ec6fbfdb04df7895c39f15f0e7479acc43bca + React-RCTNetwork: 926b436b6afada9905d969a8e3713cf204905a00 + React-RCTSettings: cc083c9b6e126b7e6ea1128e64837d8b78ceb219 + React-RCTText: c36ddf2bda5131b325e1c2763700f0a63a963e1d + React-RCTVibration: 12a2a859fa22368d2fc3ca7594504fd130b91a18 + React-runtimeexecutor: 04332dda2f2335ea4ddaf9255de069d3269f4e8b + ReactCommon: 200471e0841cf2f7cde1fa2ef3d3c199ed970c07 ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab ReactNativeExceptionHandler: 8025d98049c25f186835a3af732dd7c9974d6dce RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3 RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca RNCClipboard: f1736c75ab85b627a4d57587edb4b60999c4dd80 - RNCPicker: abc646b53a3d28ccfa3232c927a0ca52e0cf024d RNCPushNotificationIOS: 61a7c72bd1ebad3568025957d001e0f0e7b32191 - RNDateTimePicker: 7658208086d86d09e1627b5c34ba0cf237c60140 RNDeviceInfo: 5795b418ed3451ebcaf39384e6cf51f60cb931c9 RNFS: 3ab21fa6c56d65566d1fb26c2228e2b6132e5e32 - RNGestureHandler: dec4645026e7401a0899f2846d864403478ff6a5 + RNGestureHandler: 0cba6c7c51a90cd793cf2475cf7fdca613ede300 RNI18n: e2f7e76389fcc6e84f2c8733ea89b92502351fd8 RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5 RNPermissions: b3d9d00889e37cc184d365ab04bb7a3f20811b1c RNQrGenerator: 1676221c08bfabec978242989c733810dad20959 RNReactNativeHapticFeedback: 6d24decfa94e037c2ecc312407d2a057b7933f10 - RNReanimated: 7faa787e8d4493fbc95fab2ad331fa7625828cfa - RNScreens: f7ad633b2e0190b77b6a7aab7f914fad6f198d8d + RNReanimated: 60e291d42c77752a0f6d6f358387bdf225a87c6e + RNScreens: 848541d154d2a184131b34e468b10aa33008f357 RNSha256: ab608b2185fb806185a2cc112e0474065842e085 RNShare: 807d6f8231b8ebcf6dd839294b877342eb93d4e5 - RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8 - RNZendeskChat: 338c369def4e1552a533d9d34155b4b491aaf3d7 - SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 + RNSVG: 50cf2c7018e57cf5d3522d98d0a3a4dd6bf9d093 + SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 vision-camera-code-scanner: dda884a7f3ec8243a2a6d6489b91860648371bca - VisionCamera: 5fc30ecebdfd0335d11123ec9511dcbc27ee4046 - Yoga: 7a4d48cfb35dfa542151e615fa73c1a0d88caf21 + VisionCamera: e9a95af10e00aaebe99d648ff4519fd336e16ffe + Yoga: d6134eb3d6e3675afc1d6d65ccb3169b60e21980 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a - ZendeskAnswerBotProvidersSDK: 7eca94f07f10a567c631321601a645b2ba35c2da - ZendeskAnswerBotSDK: 31bd28c7c9732243d4bf3b850c441a3cdd6eba50 - ZendeskChatProvidersSDK: af93e02e2058875f92e6ad86e74ee51203b4079e - ZendeskChatSDK: fe03650a5ebe3d35fd1f4da90792021f809bcf11 - ZendeskCommonUISDK: f06dbac6c9e74c3afff75ecdc6bec3832b23258c - ZendeskCoreSDK: ce6dbb5eb4d61d2d061547a92bd0b0fe98602e9d - ZendeskMessagingAPISDK: 95a99f1eab9482b4106ec88466b93a89f9f7c5fa - ZendeskMessagingSDK: 4f5f3d43766bb3b2ea6411d1331cfe609ff33618 - ZendeskSDKConfigurationsSDK: a5c21010e17b71d02bc2cfe73dcc9da1efa0a7b2 - ZendeskSupportProvidersSDK: 685b5d185af47ced0ec40564ec46355c838bbd06 - ZendeskSupportSDK: 92e6f9d334e81e9186f8a17583862350460a5393 + ZendeskAnswerBotProvidersSDK: a024260282886870a15e7a986bf5286c23fd9311 + ZendeskAnswerBotSDK: b9f74105b26fda5f74d6639c0dc8fe37f522a867 + ZendeskChatProvidersSDK: fa1c5acd7fd09157b5994fb05a3b1628409fdfd1 + ZendeskChatSDK: 40fa26f80a589eb38c6cecf17af71206da7bcaf3 + ZendeskCommonUISDK: b87f90874386ebcc01f4b2a0b20a28c62658987a + ZendeskCoreSDK: cdc814e7fd64764fb0b0acb04e596df437708faa + ZendeskMessagingAPISDK: 9ce539eebcaa014fe6acc44da31262a2c52891c1 + ZendeskMessagingSDK: ccc8dd41b774b0d2f94733505b9f1882d72184da + ZendeskSDKConfigurationsSDK: 79c444da987390456ec05657ffa2f5ca0eb54aef + ZendeskSupportProvidersSDK: 4aafe8114a9bba1a98dc03ca14d8fa6510cf4e70 + ZendeskSupportSDK: 8cf921c496269bb5f53aac9320f3d002a793991d ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 4a1130da2ee9cd7365bb5a5290feaed31a6bd470 +PODFILE CHECKSUM: 7dfdfa4afe9355d54bec685917054fc54ab9040f -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json index 2327e470f54..2b9428a8c65 100644 --- a/ios/link-assets-manifest.json +++ b/ios/link-assets-manifest.json @@ -45,6 +45,54 @@ "path": "assets/fonts/TitilliumWeb/TitilliumWeb-SemiBoldItalic.ttf", "sha1": "f81d3a5f38c6bda1fb3547bf91fe0b68b54066c5" }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Black.otf", + "sha1": "becf8bd5053f5529785c9d1d975f2d6f54446d47" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-BlackItalic.otf", + "sha1": "079c243911ef9d6cbb25f5e25b0a0fcdc80e217a" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Bold.otf", + "sha1": "5348d5fdb96da25b0b444b6cf433100e17b2c944" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-BoldItalic.otf", + "sha1": "19b6af581aa9de369c5551b5c81af4567e96fd33" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Italic.otf", + "sha1": "149caa0987bb4765e34a34ccc315d89a9d5b259c" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Light.otf", + "sha1": "096f9b521b0fc0e7ebe6c60890fbfe312e7e4b01" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-LightItalic.otf", + "sha1": "9a74303f47edd127dc6f2c48301c9b0d4ffbbf17" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Regular.otf", + "sha1": "0a8be559a9a9d8ce47eb6ca3de9db16f42a00580" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Semibold.otf", + "sha1": "fa6cff8c0046996bc9fb124abf5799408eb3b369" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-SemiboldItalic.otf", + "sha1": "8502e9c211803bb2b0446348c69cf56763c987a3" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-Thin.otf", + "sha1": "dd3f889e6ca51de7e2006418b0125c02421fe574" + }, + { + "path": "assets/fonts/TitilliumSansPro/TitilliumSansPro-ThinItalic.otf", + "sha1": "fb0f0fcc215d93fd4241f3af18408b7c42b33b3d" + }, { "path": "assets/fonts/ReadexPro/ReadexPro-Regular.ttf", "sha1": "93e4080794b725f216a94b57ed62a51bc77bce91" diff --git a/jest-e2e.config.js b/jest-e2e.config.js index 21194829495..a1a7a518bf1 100644 --- a/jest-e2e.config.js +++ b/jest-e2e.config.js @@ -1,10 +1,7 @@ module.exports = { preset: "react-native", - transform: { - "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" - }, transformIgnorePatterns: [ - "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|@react-native-community/datetimepicker)" + "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|mixpanel-react-native)" ], moduleNameMapper: { "\\.svg": "/ts/__mocks__/svgMock.js" diff --git a/jest.config.js b/jest.config.js index 2b4929b5032..5d6fdc7c042 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,15 +1,15 @@ module.exports = { preset: "react-native", - transform: { - "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" - }, transformIgnorePatterns: [ - "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|@react-native-community/datetimepicker|remark|unified|bail|is-plain-obj|trough|vfile|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|parse-entities|character-entities|mdast-util-to-markdown|zwitch|longest-streak|@pagopa/io-react-native-zendesk|rn-qr-generator)" + "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|remark|unified|bail|is-plain-obj|trough|vfile|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|parse-entities|character-entities|mdast-util-to-markdown|zwitch|longest-streak|@pagopa/io-react-native-zendesk|rn-qr-generator|mixpanel-react-native)" ], moduleNameMapper: { "\\.svg": "/ts/__mocks__/svgMock.js" }, - setupFiles: ["./jestSetup.js"], + setupFiles: [ + "./jestSetup.js", + "./node_modules/react-native-gesture-handler/jestSetup.js" + ], globalSetup: "./jestGlobalSetup.js", setupFilesAfterEnv: [ "@testing-library/jest-native/extend-expect", diff --git a/jest.config.no.timezone.js b/jest.config.no.timezone.js index c9197d78ffd..fd0e92bebe9 100644 --- a/jest.config.no.timezone.js +++ b/jest.config.no.timezone.js @@ -4,12 +4,15 @@ module.exports = { "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" }, transformIgnorePatterns: [ - "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|@react-native-community/datetimepicker|remark|unified|bail|is-plain-obj|trough|vfile|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|parse-entities|character-entities|mdast-util-to-markdown|zwitch|longest-streak|@pagopa/io-react-native-zendesk)" + "node_modules/(?!(jest-)?@react-native|react-native|react-navigation|@react-navigation|react-navigation-redux-helpers|react-native-device-info|native-base|native-base-shoutem-theme|@shoutem/animation|@shoutem/ui|rn-placeholder|jsbarcode|@pagopa/react-native-cie|react-native-share|jail-monkey|@react-native-community/art|@react-native-community/push-notification-ios|@react-native-camera-roll/camera-roll|@codler|remark|unified|bail|is-plain-obj|trough|vfile|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|parse-entities|character-entities|mdast-util-to-markdown|zwitch|longest-streak|@pagopa/io-react-native-zendesk|mixpanel-react-native)" ], moduleNameMapper: { "\\.svg": "/ts/__mocks__/svgMock.js" }, - setupFiles: ["./jestSetup.js"], + setupFiles: [ + "./jestSetup.js", + "./node_modules/react-native-gesture-handler/jestSetup.js" + ], setupFilesAfterEnv: [ "@testing-library/jest-native/extend-expect", "./jestSetupAfterEnv.js" diff --git a/jestSetup.js b/jestSetup.js index 60213bda3a4..97170930ce8 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -11,21 +11,10 @@ import mockRNDeviceInfo from "react-native-device-info/jest/react-native-device- import mockRNCameraRoll from "@react-native-camera-roll/camera-roll/src/__mocks__/nativeInterface"; import mockZendesk from "./ts/__mocks__/io-react-native-zendesk.ts"; -// eslint-disable-next-line functional/immutable-data -NativeModules.RNGestureHandlerModule = { - attachGestureHandler: jest.fn(), - createGestureHandler: jest.fn(), - dropGestureHandler: jest.fn(), - updateGestureHandler: jest.fn(), - forceTouchAvailable: jest.fn(), - State: {}, - Directions: {} -}; - jest.mock("@pagopa/io-react-native-zendesk", () => mockZendesk); jest.mock("@react-native-async-storage/async-storage", () => mockAsyncStorage); -jest.mock("@react-native-community/push-notification-ios", jest.fn()); -jest.mock("@react-native-cookies/cookies", jest.fn()); +jest.mock("@react-native-community/push-notification-ios", () => jest.fn()); +jest.mock("@react-native-cookies/cookies", () => jest.fn()); jest.mock("react-native-share", () => jest.fn()); jest.mock("@react-native-clipboard/clipboard", () => mockClipboard); jest.mock("@react-native-camera-roll/camera-roll", () => mockRNCameraRoll); @@ -47,8 +36,8 @@ jest.mock("react-native-reanimated", () => { }); jest.mock("react-native-blob-util", () => ({ - DocumentDir: jest.fn(), - polyfill: jest.fn() + DocumentDir: () => jest.fn(), + polyfill: () => jest.fn() })); // eslint-disable-next-line functional/immutable-data @@ -68,16 +57,16 @@ global.fetch = nodeFetch; // eslint-disable-next-line @typescript-eslint/no-explicit-any,functional/immutable-data global.AbortController = AbortController; -jest.mock("remark-directive", jest.fn()); -jest.mock("remark-rehype", jest.fn()); -jest.mock("rehype-stringify", jest.fn()); -jest.mock("rehype-format", jest.fn()); -jest.mock("unist-util-visit", jest.fn()); -jest.mock("hastscript", jest.fn()); +jest.mock("remark-directive", () => jest.fn()); +jest.mock("remark-rehype", () => jest.fn()); +jest.mock("rehype-stringify", () => jest.fn()); +jest.mock("rehype-format", () => jest.fn()); +jest.mock("unist-util-visit", () => jest.fn()); +jest.mock("hastscript", () => jest.fn()); jest.mock("react-native-device-info", () => mockRNDeviceInfo); -global.__reanimatedWorkletInit = jest.fn(); +global.__reanimatedWorkletInit = () => jest.fn(); jest.mock("@gorhom/bottom-sheet", () => { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -105,3 +94,32 @@ jest.mock("react-native-pdf", () => jest.fn()); jest.mock("react-native-permissions", () => require("react-native-permissions/mock") ); + +/* + * Turbo modules mocks. + */ + +jest.mock("react-native/Libraries/TurboModule/TurboModuleRegistry", () => { + const turboModuleRegistry = jest.requireActual( + "react-native/Libraries/TurboModule/TurboModuleRegistry" + ); + return { + ...turboModuleRegistry, + getEnforcing: name => { + // List of TurboModules libraries to mock. + const modulesToMock = ["RNDocumentPicker"]; + if (modulesToMock.includes(name)) { + return null; + } + return turboModuleRegistry.getEnforcing(name); + } + }; +}); + +jest.mock("mixpanel-react-native", () => ({ + __esModule: true, + default: () => jest.fn(), + Mixpanel: jest.fn(() => ({ + init: jest.fn() + })) +})); diff --git a/locales/de/index.yml b/locales/de/index.yml index 1a5c90fb118..a2d6438f7f9 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -248,7 +248,6 @@ permissionRationale: title: "Zugriff auf die Kamera gewähren" message: "Wir brauchen Zugang zur Kamera, um die Taschenlampenfunktion zu benutzen." startup: - title: "Initialisierung" authentication: "Benutzer authentifiziert" sessionInfo: "SPID-Attribute geladen" profileInfo: "Benutzerprofil geladen" @@ -534,11 +533,7 @@ authentication: hint: "Karussell mit einer einführenden Anleitung zur IO-Funktionalität. Wähle, um den Inhalt kennenzulernen." session_expired: title: "Deine Sitzung ist abgelaufen" - body: "Aus Sicherheitsgründen bitten wir dich {{days}} Tage nach deiner letzten Anmeldung mit SPID oder CIE, deine Anmeldedaten erneut einzugeben, um IO zu benutzen." - cie_unsupported: - title: "Anmeldung mit CIE nicht möglich" - os_version_unsupported: "ein Betriebssystem mit Android 6.0 oder höher" - nfc_incompatible: "ein kompatibler NFC-Sensor" + body: "Aus Sicherheitsgründen bitten wir dich, dich alle {{days}} Tage oder wenn du die App verlässt, mit SPID oder CIE anzumelden." contentTitleCie: "Du hast keinen SPID oder CIE?" spid_or_cie: "Um IO nutzen zu können, musst du über eine dieser digitalen Werkzeuge verfügen, die deine Identität in sicherer Weise bestätigen." request_cie: "CIE anfordern" @@ -549,6 +544,7 @@ authentication: loginSpidCie: "Anmeldung mit SPID oder CIE" infoSpidCie: "Einführung in SPID und CIE" loginSpidCieContent: "Ab heute kannst du dich zusätzlich zu SPID mit deiner elektronischen Identitätskarte (CIE) anmelden" + privacyLink: "Datenschutzerklärung" nospid: "Du hast keinen SPID? Erfahre mehr" nospid-nocie: "Du hast keinen SPID und keine CIE? Erfahre mehr" card1-title: "Alle Mitteilungen an einem Ort" @@ -557,10 +553,8 @@ authentication: card2-content: "Du kannst eine Steuer oder eine Strafe über dein Bankkonto oder mit deiner Bankkarte bezahlen" card3-title: "Bezahle schnell eine Aufforderung in Papierform" card3-content: "Durch das Scannen des QR-Codes kannst du schnell und einfach eine Zahlungsaufforderung in Papierform bezahlen" - card4-title: "Du kannst dich nur mit SPID anmelden" - card4-content: "Das von dir verwendete Gerät unterstützt den Zugang mit einer elektronischen Identitätskarte (CIE) nicht" - card5-title: "Willkommen bei der ersten öffentlichen Version von IO!" - card5-content: "In der rechten oberen Ecke findest du das Symbol '?', mit dem du einen Chat mit dem IO-Team beginnen kannst, wenn etwas nicht so funktioniert, wie es sollte." + card5-title: "Willkommen in IO!" + card5-content: "Lass uns gemeinsam herausfinden, was du machen kannst" card5-content-accessibility: "Wähle das vorherige Element, um einen Chat mit dem IO-Team zu beginnen, wenn etwas nicht so funktioniert, wie es sollte." expiredCardTitle: "Karte abgelaufen oder nicht mehr gültig" expiredCardHeaderTitle: "Anmeldung mit CIE" @@ -646,12 +640,12 @@ authentication: contextualHelpContent: "Hier findest du Informationen darüber, wie du dich bei IO authentifizieren kannst, sowie einige Sicherheitstipps." news: "Neu" title: "Ab sofort kannst du leichter auf IO zugreifen" - identity_check: "Du musst dich nicht mehr alle 30 Tage mit SPID oder CIE authentifizieren, sondern nur noch einmal im Jahr oder wenn du die App verlässt" + identity_check: "Du musst dich nicht mehr alle 30 Tage mit SPID oder CIE identifizieren, sondern nur noch einmal im Jahr oder wenn du die App verlässt" security_suggests: "Einige Sicherheitstipps" passcode: "In allen anderen Fällen reicht dein Gesicht, dein Fingerabdruck oder dein Entsperrcode aus." notification: "Du erhältst bei jedem neuen Zugang eine E-Mail. Wenn du diese nicht erkennst, kannst dz den Zugang zu IO sperren." button_accept_lv: "Ja, Schnellzugriff einrichten" - button_decline_lv: "Nein, ich möchte mich alle 30 Tage neu anmelden" + button_decline_lv: "Melde dich alle 30 Tage mit SPID oder CIE an" security_suggestions: fingerprint: "Gib deinen biometrischen Erkennungs- oder Entsperrcode nicht an Dritte weiter." io_logout: "Wenn du dieses Gerät verlierst, beende IO über die Website" @@ -715,10 +709,6 @@ authentication: email: cduScreens: validateMail: - title: "Du hast deine E-Mail-Adresse nicht bestätigt" - subtitle: "Um die App weiter nutzen zu können, musst du deine E-Mail-Adresse bestätigen" - editButton: "E-Mail-Adresse ändern" - validateButton: "E-Mail-Adresse bestätigen" header: title: "IO konfigurieren" help: @@ -726,7 +716,6 @@ email: emailAlreadyTaken: title: "Deine E-Mail-Adresse ändern" subtitleStart: "Deine E-Mail-Adresse" - subtitleEnd: "wird bereits auf IO verwendet; du musst eine andere eingeben, um die App weiter benutzen zu können." editButton: "E-Mail-Adresse ändern" header: title: "IO konfigurieren" @@ -756,7 +745,7 @@ email: label: "Persönliche E-Mail Adresse" alert: title: "Die E-Mail {{email}}, die mit Ihrer SPID-Identität verknüpft ist, wird bereits auf IO verwendet, bitte geben Sie eine andere ein." - description: "E-Mail bereits in Gebrauch" + description1: "E-Mail bereits in Gebrauch" modaltitle: "Diese E-Mail ist bereits in Gebrauch" modaldescription: "Dies kann der Fall sein, wenn Sie dieselbe Adresse wie ein Familienmitglied haben" modalbutton: "Andere E-Mail verwenden" @@ -785,6 +774,7 @@ email: title: "Bestätige deine E-Mail-Adresse" subtitle: "Folge den Anweisungen, die an folgende Adresse gesendet wurden:" link: "Ist sie nicht korrekt?" + countdowntext: "Eine neue E-Mail anfordern in " buttonlabelsent: "E-Mail gesendet" buttonlabelsentagain: "E-Mail erneut senden" toast: "Erledigt! Prüf deinen Posteingang." @@ -970,6 +960,11 @@ wallet: title: "Möchtest du in der App bezahlen?" content: "Finde heraus, welche Zahlungsmethoden du in dein Konto aufnehmen kannst." cta: "Mehr erfahren" + error: + title: "Es ist ein Fehler aufgetreten" + subtitle: "Versuche es erneut oder kontaktiere den Support" + primaryButton: "Schließen" + secondaryButton: "Erneut versuchen" wallet: "Konto" refreshWallet: "Konto aktualisieren" favourite: @@ -1001,7 +996,7 @@ wallet: text3: "Wenn du möchtest, kannst du die Bank wählen, bei der du eine aktive PagoBANCOMAT-Karte hast. " text4: "Liste der verfügbaren Banken" bpay: - title: "Zahle auf IO mit BANCOMAT Pay" + title: "Zahle auf IO mit BANCOMAT-Pay" description: text1: "Füge BANCOMAT Pay hinzu, um pagoPA-Zahlungsaufforderungen einfacher zu bezahlen sowie an den auf IO verfügbaren Initiativen teilzunehmen.\nWenn du fortfährst, akzeptierst du die " text2: "Datenschutzbestimmungen." @@ -1023,7 +1018,7 @@ wallet: creditCard: "{{brand}}-Karte, die Kartennummer endet mit {{blurredNumber}}" bancomat: "BANCOMAT {{bankName}}" coBadge: "{{brand}}-Karte, {{bankName}}" - bancomatPay: "BANCOMATPay {{bankName}}" + bancomatPay: "BANCOMAT Pay {{bankName}}" cta: "Details zur Zahlungsmethode anzeigen" bankNotAvailable: "Bankenname nicht verfügbar" addElement: "Element zum Konto hinzufügen" @@ -1037,7 +1032,7 @@ wallet: bancomat: headerTitle: "PagoBANCOMAT-Karte hinzufügen" koTimeout: - title: "Wir konnten deine PagoBANCOMAT Karten nicht finden" + title: "Wir konnten deine PagoBANCOMAT-Karten nicht finden" body: "Versuche es erneut" koNotFound: title: "Wir konnten keine Karten finden" @@ -1155,10 +1150,11 @@ wallet: title: "Ich weiß es nicht" description: "Wir durchsuchen alle auf deinem Namen lautenden Karten bei den am Dienst teilnehmenden Banken" generalError: "Beim Hinzufügen der Zahlungsmethode trat ein allgemeiner Fehler auf" - success: - title: "Die Karte wurde hinzugefügt!" - continueButton: "Weiter" - failure: + outcome: + SUCCESS: + title: "Die Karte wurde hinzugefügt!" + subtitle: "" + primaryAction: "Weiter" GENERIC_ERROR: title: "Es ist ein unerwarteter Fehler aufgetreten" subtitle: "Versuche es erneut oder kontaktiere den Support" @@ -1184,6 +1180,10 @@ wallet: title: "Du hast diese Zahlungsmethode bereits hinzugefügt" subtitle: "" primaryAction: "Schließen" + BPAY_NOT_FOUND: + title: "Wir haben kein aktives BANCOMAT Pay Konto gefunden." + subtitle: "Wende dich an deine Bank, um den Dienst zu aktivieren." + primaryAction: "Schließen" alert: supportedCardPageLinkError: "Beim Öffnen der Übersichtsseite der unterstützten Karten ist ein Fehler aufgetreten." msgErrorUpdateApp: "Beim Öffnen des App-Stores ist ein Fehler aufgetreten" @@ -1687,6 +1687,12 @@ wallet: subtitle: "Es wurde kein Betrag abgebucht.\nDu hast wahrscheinlich die Obergrenze deiner Zahlungsmethode überschritten." INVALID_METHOD: title: "Die Zahlungsmethode wird nicht unterstützt" + WAITING_CONFIRMATION_EMAIL: + title: "Warte auf eine Bestätigung per E-Mail" + subtitle: "Es war nicht möglich, das Ergebnis der Zahlung zu überprüfen. Wenn sie erfolgreich war, wirst du in Kürze eine E-Mail erhalten. Sollte diese nicht ankommen, kontaktiere bitte den Support." + METHOD_NOT_ENABLED: + title: "Zahlungsmethode in der App aktivieren" + subtitle: "Bevor du die Zahlung erneut versuchst, wähle die Zahlungsmethode in deinem Konto aus und aktiviere die Funktion “Zahlungen in der App”." support: button: "Support kontaktieren" supportTitle: "Support kontaktieren" @@ -1819,6 +1825,7 @@ messages: success: "Wiederherstellung durchgeführt." failure: "Die Wiederherstellung ist fehlgeschlagen" messageDetails: + accessibilityAttachmentIcon: "Enthält Anhänge" contextualHelpTitle: "Mitteilungsdetails" headerTitle: "Mitteilung" emptyMessage: "Kein Inhalt" @@ -1857,13 +1864,18 @@ messageDetails: action: "Mehr erfahren" bottomSheet: title: "Was ist eine dynamische Mitteilung?" - body: "Die versendende Körperschaft kann den Inhalt dieser Nachricht auch nach dem Versand aktualisieren, um sicherzustellen, dass die Informationen stets korrekt und relevant sind.\n\n Dies geschieht in bestimmten Fällen, wie z. B. bei der Aktualisierung veralteter Informationen oder nicht mehr gültiger Anlagen.\n\n Wenn die Körperschaft dich über neue Informationen oder wichtige Aktualisierungen informieren muss, wird sie dies mittels einer neuen Mitteilung tun." + body: "Die versendende Körperschaft kann den Inhalt dieser Nachricht auch nach dem Versand aktualisieren, um sicherzustellen, dass die Informationen stets korrekt und relevant sind.\n\n Dies geschieht in bestimmten Fällen, wie z. B. bei der Aktualisierung veralteter Informationen oder nicht mehr gültiger Anhänge.\n\n Wenn die Körperschaft dich über neue Informationen oder wichtige Aktualisierungen informieren muss, wird sie dies mittels einer neuen Mitteilung tun." messagePDFPreview: title: "PDF-Vorschau" - singleBtn: "Speichern oder freigeben" + singleBtn: "Speichern oder teilen" + singleBtnAccessibility: "Dokument speichern oder teilen" open: "Öffnen" save: "Speichern" + saveAccessibility: "Dokument speichern" savedAtLocation: "Das Dokument {{name}} wurde im Ordner Download gespeichert." + share: "Teilen" + shareAccessibility: "Dokument teilen" + pdfAccessibility: "Vorschau PDF-Dokument" errors: previewing: title: "Keine Vorschau verfügbar" @@ -1976,16 +1988,13 @@ serviceDetail: disabled: "deaktiviert" notValidated: "Um die E-Mail-Weiterleitung zu aktivieren, musst du deine E-Mail-Adresse bestätigen." identification: - title: "Hallo!" + title: "Hallo {{name}}!" titleProfileName: "Hallo {{profileName}}!" titleValidation: "Bestätige den Vorgang" logout: "Abmelden" logoutProfileName: "Du bist nicht {{profileName}}?" logoutDescription: "Du kannst dich mit deinem SPID oder deiner CIE anmelden. Diese Sitzung wird abgemeldet." - logoutDescriptionProfileName: "Du kannst dich mit deinem SPID oder deiner CIE anmelden. Die Sitzung von {{profileName}} wird abgemeldet." - subtitleCode: "Gib deinen Entsperrcode ein" - subtitleCodeFingerprint: "Verwende den Fingerabdruck oder gib den Entsperrcode ein" - subtitleCodeFaceId: "Verwende FaceID oder gib den Entsperrcode ein" + logoutDescriptionProfileName: "Du kannst dich mit deinem SPID oder deiner CIE anmelden. Die Sitzung von {{profileName}} wird abgemeldet." biometric: fingerprintType: "Fingerabdruck" title: "Biometrische Identifikation" @@ -1993,10 +2002,7 @@ identification: reason: "Identifikation durch TouchID fehlgeschlagen" title: "Identifikation erforderlich" sensorDescription: "Berührungssensor" - fail: - remainingAttempts: "Du hast noch {{attempts}} Versuche übrig." - remainingAttemptSingle: "Du hast noch einen Versuch übrig." - tooManyAttempts: "Zu viele falsche Versuche." + fail: waitMessage: "Versuche es erneut in:" unlockCode: accessibility: @@ -2220,12 +2226,20 @@ bonus: other: "{{seconds}} Sekunden" features: messages: + attachmentDownloadFeedback: "Download läuft" + attachments: "Anhänge" loading: title: "Die Details der Mitteilung werden geladen" subtitle: "Warte ein paar Sekunden" updateBottomSheet: title: "Aktualisiere die IO-App" subtitle: "App IO Version {{value}} ist erforderlich, um diese Funktion zu nutzen. Bitte aktualisiere die App, um fortzufahren." + badge: + dueDate: "Abgelaufen am {{date}} um {{time}}" + alert: + addReminder: "Erinnerung hinzufügen" + removeReminder: "Erinnerung entfernen" + content: "Läuft am {{date}} um {{time}} ab" euCovidCertificate: save: album: "IO" @@ -2284,13 +2298,15 @@ features: activated: "Vielen Dank, der Dienst ist aktiv!" error: "Bei deiner Anfrage ist ein Fehler aufgetreten" details: + badge: + legalValue: "Rechtsgültig" title: "Details der Mitteilung" noticeCode: "Zahlungskodex" loadError: title: "Etwas ist schief gelaufen" body: "Die Details zu deiner Mitteilung konnten nicht abgerufen werden. Bitte versuche es erneut" cancelledMessage: - body: "Dieser Bescheid wurde vom Absender gelöscht. Du kannst dessen Inhalt ignorieren." + body: "Diese Zustellung wurde vom Absender gelöscht. Du kannst dessen Inhalt ignorieren." unpaidPayments: "Für die Erstattung von Zahlungen im Zusammenhang mit diesem Bescheid wende dich bitte an den Absender." payments: "Zahlungen" paymentSection: @@ -2306,10 +2322,10 @@ features: bottomSheet: title: "Beiliegende F24-Formulare" infoSection: - title: "Informationen zum Bescheid" + title: "Informationen zur Zustellung" iun: "IUN-Code" timeline: - title: "STATUS DES BESCHEIDS" + title: "STATUS DER ZUSTELLUNG" status: DELIVERED: "Zugestellt" DELIVERING: "In Zustellung" @@ -2331,7 +2347,7 @@ support: helpCenter: header: "Hilfe" cta: - contactSupport: "Erstelle ein Ticket" + contactSupport: "Ticket erstellen" seeReports: "Offene Tickets" supportComponent: title: "Benötigst du Hilfe?" @@ -2420,13 +2436,10 @@ fastLogin: userInteraction: sessionExpired: noPin: - title: "Es ist zu lange her!" - subtitle: "Aus Sicherheitsgründen musst du dich erneut mit SPID oder CIE authentifizieren. Anschließend kannst du die IO-Konfiguration dort fortsetzen, wo du aufgehört hast." + title: "Es ist zu lange her!" submitButtonTitle: "Fortfahren" continueNavigation: - title: "Deine Sitzung ist abgelaufen" - subtitle: "Um die App weiter zu verwenden, drück auf “Fortsetzen”." - submitButtonTitle: "Fortsetzen" + title: "Deine Sitzung ist abgelaufen" cancelButtonTitle: "Abbrechen" transientError: title: "Wir konnten dich nicht authentifizieren" diff --git a/locales/de/jailbroken_body.md b/locales/de/jailbroken_body.md index f7d5b7296cf..9762448e8c1 100644 --- a/locales/de/jailbroken_body.md +++ b/locales/de/jailbroken_body.md @@ -1,7 +1,7 @@ -Es scheint, dass ein **Jailbreak** auf Ihrem Gerät durchgeführt wurde. +Sul tuo dispositivo è stato effettuato un **jailbrack**. Questo succede quando il sistema operativo viene modificato o installato in modo anomalo. Questo potrebbe compromettere l’integrità del sistema e l'app IO potrebbe non funzionare correttamente. -Dies geschieht, wenn das Betriebssystem verändert oder nicht ordnungsgemäß installiert wurde. Die Integrität des Betriebssystems kann beeinträchtigt sein, und die IO-App funktioniert möglicherweise nicht richtig. +**Cosa fare?** -Wenn möglich, fahren Sie bitte nicht fort und installieren Sie die IO-App auf einem anderen Gerät mit einem intakten und aktuellen Betriebssystem. +Ti invitiamo a usare l'app IO su un dispositivo diverso, con un sistema operativo integro e aggiornato. -Wenn Sie fortfahren, erklären Sie sich damit einverstanden, die angebotenen Dienste auf eigenes Risiko zu nutzen, da mangelnde Integrität zu Datendiebstahl und/oder anderen betrügerischen Aktivitäten führen kann. Sie erklären sich ferner damit einverstanden, dass der App-Entwickler in keiner Weise für Schäden, Verluste oder Folgen verantwortlich ist, die Ihnen durch die Nutzung der IO-App in diesem Ausführungskontext entstehen. +Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti. La mancanza di integrità può portare al furto di dati o ad altre attività fraudolente. Accetti anche che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze causate dall'app IO in questo contesto di esecuzione. \ No newline at end of file diff --git a/locales/de/rooted_body.md b/locales/de/rooted_body.md index cc9e5a0839e..f22ce4f6fbf 100644 --- a/locales/de/rooted_body.md +++ b/locales/de/rooted_body.md @@ -1,7 +1,7 @@ -Es scheint, dass **Root-Rechte** auf Ihrem Gerät aktiviert sind. +Sul tuo dispositivo sono attivi i **permessi di root**. Questo succede quando il sistema operativo viene modificato o installato in modo anomalo. Questo potrebbe compromettere l’integrità del sistema e l'app IO potrebbe non funzionare correttamente. -Dies geschieht, wenn das Betriebssystem verändert oder nicht ordnungsgemäß installiert wurde. Die Integrität des Betriebssystems kann beeinträchtigt sein, und die IO-App funktioniert möglicherweise nicht richtig. +**Cosa fare?** -Wenn möglich, fahren Sie bitte nicht fort und installieren Sie die IO-App auf einem anderen Gerät mit einem intakten und aktuellen Betriebssystem. +Ti invitiamo a usare l'app IO su un dispositivo diverso, con un sistema operativo integro e aggiornato. -Wenn Sie fortfahren, erklären Sie sich damit einverstanden, die angebotenen Dienste auf eigenes Risiko zu nutzen, da mangelnde Integrität zu Datendiebstahl und/oder anderen betrügerischen Aktivitäten führen kann. Sie erklären sich ferner damit einverstanden, dass der App-Entwickler in keiner Weise für Schäden, Verluste oder Folgen verantwortlich ist, die Ihnen durch die Nutzung der IO-App in diesem Ausführungskontext entstehen. +Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti. La mancanza di integrità può portare al furto di dati o ad altre attività fraudolente. Accetti anche che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze causate dall'app IO in questo contesto di esecuzione. \ No newline at end of file diff --git a/locales/en/cie/cieNotSupported.md b/locales/en/cie/cieNotSupported.md deleted file mode 100644 index 3a9a4cd82e5..00000000000 --- a/locales/en/cie/cieNotSupported.md +++ /dev/null @@ -1,4 +0,0 @@ -The device you are using does not support login with Electronic Identity Card (CIE). Login with CIE is only supported by: - -- devices with NFC chip and Android 6.0 or higher operating system; -- iPhone 7 and above with iOS 13 or higher operating system. \ No newline at end of file diff --git a/locales/en/index.yml b/locales/en/index.yml index feca83dc088..10e9c888e5d 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -190,6 +190,7 @@ global: messages: Messages wallet: Wallet scan: Scan + payments: Payments documents: Documents profile: Profile services: Services @@ -213,6 +214,11 @@ tablet: message: The IO interface is optimized for smartphones, but all the functions compatible with your device are still available. rooted: title: Looks like the security of your device has been compromised! + body: If you press 'Continue', you agree to continue browsing at your own risk, otherwise close the app. + learnMoreButton: + title: What does it mean? + learnMoreBottomsheet: + title: Device security bodyiOS: !include jailbroken_body.md bodyAndroid: !include rooted_body.md continueAlert: @@ -229,6 +235,7 @@ locales: en: English de: German clipboard: + copyIntoClipboard: copy to clipboard copyFeedback: Copied to clipboard copyText: copy copyFeedbackButton: copied @@ -249,7 +256,7 @@ permissionRationale: title: "Allow camera access" message: "We require camera permissions to use the torch on the back of your phone." startup: - title: Initialization + title: We are getting ready for your experience on IO authentication: User authenticated sessionInfo: SPID attributes loaded profileInfo: User profile loaded @@ -437,6 +444,7 @@ profile: idpayTest: IDPay idpayTestAlert: This change requires app reboot designSystemEnvironment: Experimental Design System + newWalletSection: New wallet section appVersion: Version backendVersion: Backend Version debugMode: Debug mode @@ -447,8 +455,9 @@ profile: title: Warning message: To add a new test card, you must enable the pagoPA test environment from the settings. closeButton: Close - walletPlayground: - titleSection: "New Wallet" + itWallet: + itWalletTest: IT Wallet + itWalletTestDescription: Enable the IT Wallet test environment data: title: Your data subtitle: Here you will find your personal and contact data used by the IO app. @@ -560,13 +569,15 @@ authentication: hint: Carousel that contains an introduction guide to IO functionalities. Select to know the content. session_expired: title: Your session is expired - body: For security reasons, after {{days}} days from the last login with SPID or CIE, we ask you to sign in again to use IO. + body: For security reasons, we ask you to log in with SPID or CIE every {{days}} days or if you log out cie_unsupported: - title: Login with CIE unavailable - body: !include cie/cieNotSupported.md - android_desc: !include cie/cieNotSupported_android.md - os_version_unsupported: an Android 6.0 or higher operating system - nfc_incompatible: a NFC chip + title: You cannot use CIE + body: The device you are using does not support access with Electronic ID Card (CIE). + button: I get it + nfc_problem: To read the CIE, a device with NFC chip is required. + os_problem: An Android 6.0 or iOS 13 or higher operating system is required. + nfc_alert: Your device does not have NFC. + os_alert: The device's operating system is not up-to-date. contentTitleCie: Don't have SPID or CIE? cie_information_request: !include cie.md spid_or_cie: To use IO you must have one of these digital tools, which certify your identity. @@ -579,6 +590,7 @@ authentication: loginSpidCie: Login with SPID or CIE infoSpidCie: Introduction to SPID and CIE loginSpidCieContent: From today, in addition to SPID, you can use your Electronic ID Card to login + privacyLink: Privacy nospid: Don't have SPID? Find out more nospid-nocie: Don't have SPID or CIE? Find out more card1-title: All messages in one place @@ -587,11 +599,9 @@ authentication: card2-content: You can pay a tax or a penalty by your bank account and your cards card3-title: Quickly pay a physical notice card3-content: You can pay a notice also by scanning the QR code on the physical notice - card4-title: You can login only with SPID - card4-content: The device you are using does not support access with Electronic Identity Card (CIE) - card5-title: Welcome to the public version of IO! - card5-content: In this version you can find in the upper-right corner the icons to report bugs or start a chat with the development team. We're waiting for your feedback! - card5-content-accessibility: Seleziona elemento precedente per aprire una chat con il team di IO se qualcosa non funziona come dovrebbe. + card5-title: Welcome to IO! + card5-content: What can you do? + card5-content-accessibility: Select previous item to open a chat with the IO team if something is not working. expiredCardTitle: Card expired or no longer valid expiredCardHeaderTitle: Login with CIE expiredCardContent: "The card used is no longer valid or has expired.\nIf you want to log in and access IO you must use SPID." @@ -654,6 +664,7 @@ authentication: cardLocked: CIE card locked wrongPin1AttemptLeft: Wrong PIN, you still have 1 attempt wrongPin2AttemptLeft: Wrong PIN, you still have 2 attempts + genericError: Something went wrong! Please try again. error: readerCardLostTitle: Hold the card again for a few seconds readerCardLostTitleiOS: You removed the card too soon. @@ -708,6 +719,8 @@ authentication: recoverDescription: If you're missing your password, you can recover it by tapping on the following button. dualRecoverDescription: If you're missing your credentials, you can recover them by tapping on the following buttons. recoverPassword: Recover the password + idp_login_success: + contentTitle: Hi, {{name}}! errors: notoken: The session is not ready or is invalid logout: Error on logout @@ -752,10 +765,10 @@ authentication: email: cduScreens: validateMail: - title: Email address not validated - subtitle: To continue using the app, you must validate your email address - editButton: Edit email address - validateButton: Validate email address + title: Is this your email? + subtitle: To continue using the app, you must confirm that your email is + editButton: No, change email + validateButton: Yes, confirm header: title: Configure IO help: @@ -790,17 +803,20 @@ email: newinsert: header: "Configure IO" title: "What is your email address?" - subtitle: "We use email for some communications, such as payment receipts or messages exchanged with support." + subtitle: We will use it to send you communications such as payment receipts or support messages label: "Personal email address" alert: - title: "The email address {{email}} associated with your SPID identity is already in use on IO. Please enter a different one." - description: "email address already in use" - modaltitle: "This email address is already in use" + title: "{{email}} is already used, insert another one" + description1: email address already in use + description2: Enter an email address different from your current one + description3: Already using this email address + invalidemail: Enter a valid email address + modaltitle: This email address is already in use modaldescription: "This can happen if you share the same address with a family member" - modalbutton: "Use another email address" + modalbutton: Use another email address edit: title: Edit email - subtitle: The email you inserted is + subtitle: Now you use validated: The email you inserted and validated is label: New personal email address cta: Edit email @@ -820,10 +836,11 @@ email: validated: Email validated! validated_ok: Thank you, now you can use all payment features, messages forwarding, etc. newvalidate: - title: Validate your email - subtitle: Follow the instructions we sent to - link: Is the email address wrong? + title: We have sent you an email + subtitle: To confirm your address, follow the instructions we have sent to + link: Isn't that right? buttonlabelsent: Email sent! + countdowntext: Request new email in buttonlabelsentagain: Send email again toast: Done! Check your mailbox. newvalidemail: @@ -925,6 +942,18 @@ datetimes: todayAt: today, at notValid: Invalid date. payment: + homeScreen: + title: Payments + methodsSection: + header: Payment methods + headerCTA: Add + historySection: + header: Transaction history + headerCTA: View all + transactionStatusBadges: + failure: "FALLITA" + success: "" + CTA: Paga un avviso alertNoPaymentMethods: title: Add a method to make in-app payments message: The method will be saved in your Wallet, so you can pay easier next time. @@ -1202,10 +1231,11 @@ wallet: title: "I don't know" description: "We will search for all cards registered to you at participating banks" generalError: "There was a generic error while adding the payment method" - success: - title: "Payment method added successfully!" - continueButton: "Vedi dettagli" - failure: + outcome: + SUCCESS: + title: "Payment method added successfully!" + subtitle: "" + primaryAction: "Vedi dettagli" GENERIC_ERROR: title: Si è verificato un errore imprevisto subtitle: Riprova o contatta l'assistenza @@ -1231,6 +1261,10 @@ wallet: title: Hai già aggiunto questo metodo subtitle: "" primaryAction: Chiudi + BPAY_NOT_FOUND: + title: Non abbiamo trovato alcun BANCOMAT Pay attivo + subtitle: "Contatta la tua Banca per attivare il servizio." + primaryAction: Chiudi alert: supportedCardPageLinkError: An error occurred while opening the supported cards page. msgErrorUpdateApp: "An error occurred while opening the app store" @@ -1740,12 +1774,19 @@ wallet: subtitle: Per maggiori informazioni, contatta la tua banca. CANCELED_BY_USER: title: L’operazione è stata annullata - subtitle: Non è stato addebitato alcun importo. + subtitle: Se vuoi pagare l’avviso, attendi qualche minuto prima di riprovare. EXCESSIVE_AMOUNT: title: Autorizzazione negata subtitle: Non è stato addebitato alcun importo.\nProbabilmente hai sforato i limiti di spesa previsti dalla tua banca. INVALID_METHOD: title: Il metodo di pagamento non è supportato + WAITING_CONFIRMATION_EMAIL: + title: Attendi la conferma via email + subtitle: Riceverai l'esito del pagamento all'indirizzo {{email}} + defaultSubtitle: Riceverai l'esito del pagamento all'indirizzo email associato. + METHOD_NOT_ENABLED: + title: Abilita il metodo per effettuare pagamenti in app + subtitle: Prima di riprovare con il pagamento, seleziona il metodo nel tuo Portafoglio e abilita la funzionalità “Pagamenti in app”. support: button: "Contatta l'assistenza" supportTitle: Contatta l'assistenza @@ -1879,6 +1920,7 @@ messages: success: "Successfully unarchived." failure: "Failed to unarchive" messageDetails: + accessibilityAttachmentIcon: "Includes attachments" contextualHelpTitle: Message details contextualHelpContent: !include messages/message_detail.md headerTitle: Message @@ -1920,12 +1962,30 @@ messageDetails: bottomSheet: title: "What's a dynamic message?" body: "It's a message that the sender can edit after you've received it. This way, the information it includes will always be up-to-date.\n\nThe sender can edit the information in a dynamic message only in specific cases. For example, to change outdated information or replace an attachment that is no longer valid.\n\nIf the sender needs to share new information or important updates with you, you'll receive a new message." + footer: + contacts: "Contact the sender" + showMoreData: "Show more data" + showMoreDataBottomSheet: + title: "Identification data" + messageId: "Message ID" + messageIdAccessibility: "ID messaggio, copy" + contactsBottomSheet: + title: "Support" + body: "If you are unclear about the content of the message, contact the Institution that sent it to you." + actions: + call: Call + email: Send email messagePDFPreview: title: "PDF Preview" singleBtn: "Save or share" + singleBtnAccessibility: "Save or share document" open: "Open" save: "Save" + saveAccessibility: "Save document" savedAtLocation: "Document {{name}} was successfully saved in your Download folder." + share: "Share" + shareAccessibility: "Share document" + pdfAccessibility: "Preview of the PDF document" errors: previewing: title: "Preview is not available" @@ -1975,6 +2035,7 @@ services: value: "Quick setup" label: "all the services you didn't disable can contact you." title: "Use quick setup" + successAlert: "You have enabled the quick setup" body: text1: "You'll receive messages only by the institutions which have something important to tell you." text2: "You can always deactivate communications by services that do not interest you." @@ -1982,6 +2043,7 @@ services: value: "Manual setup" label: "only the services you enabled can contact you." title: "Configure manually" + successAlert: "You have enabled the manual configuration" body: text1: "No one will contact you. To receive messages, you will have to enable services one by one." bottomSheet: @@ -2023,6 +2085,17 @@ services: disableAllMsg: "Remember, on IO you will receive messages only from services having some personalized information to communicate to you. You will not receive spam messages. If you disable all the services, they can no longer contact you through the app (until you enable it again). You can use the services through other channels (front office, website, etc.)" close: Close emptyListMessage: There are no services available at this time, pull down to refresh + details: + failure: + title: "There is a temporary problem. Please try again" + subtitle: "We are retrieving service details" + back: "Back" + retry: "Retry" + preferences: + title: "This service can" + inbox: "Contact you in app" + pushNotifications: "Send you push notifications" + messageReadStatus: "Receive read receipts" serviceDetail: fiscalCode: "Organization's fiscal code" fiscalCodeAccessibility: "Organization's fiscal code" @@ -2041,16 +2114,21 @@ serviceDetail: disabled: disabled notValidated: To enable the email forwarding you must validate your email address. identification: - title: Hi! + instructions: + unlockCode: unlock code + unlockCodepPrefix: use your + fingerprint: fingerprint + fingerprintPrefix: use your + faceId: face + faceIdPrefix: use your + congiunction: or + title: Hi {{name}}! titleProfileName: Hi {{profileName}}! titleValidation: Confirm the procedure logout: Logout logoutProfileName: Not {{profileName}}? logoutDescription: You can login with your SPID or CIE credentials. This session will be disconnected. logoutDescriptionProfileName: You can login with your SPID or CIE credentials. {{profileName}}'s session will be disconnected. - subtitleCode: Enter the unlock code - subtitleCodeFingerprint: use the fingerprint or the unlock code - subtitleCodeFaceId: use the Face ID or enter the unlock code biometric: fingerprintType: Fingerprint title: Biometric identification @@ -2059,8 +2137,8 @@ identification: title: Identification required sensorDescription: Login quickly fail: - remainingAttempts: You have {{attempts}} attempts left. - remainingAttemptSingle: You have {{attempts}} attempt left. + remainingAttempts: You have {{attempts}} attempts left + remainingAttemptSingle: You have {{attempts}} attempt left tooManyAttempts: Too many attempts. waitMessage: "Try again in:" unlockCode: @@ -2620,12 +2698,24 @@ bonus: conjunction: " and " features: messages: + attachmentDownloadFeedback: "Download in progress" + attachments: "Attachments" loading: title: "Loading message details" subtitle: "Please wait" updateBottomSheet: title: "Aggiorna l'app IO" subtitle: "Per usare questa funzionalità è richiesta la versione {{value}} di app IO. Aggiorna l’app per continuare." + badge: + dueDate: Expired on {{date}} at {{time}} + alert: + addReminder: Add reminder + removeReminder: Remove reminder + content: Expires on {{date}} at {{time}} + payments: + title: "pagoPA notices" + noticeCode: "Notice code" + pay: "Pay" euCovidCertificate: save: album: "IO" @@ -2778,6 +2868,10 @@ features: content: "Do you want to quit signing **{{dossierTitle}}**?" cancel: "Quit signing" confirm: "Keep signing" + alert: + title: "Do you want to stop the operation?" + confirm: "Yes, stop it" + cancel: "No, go back" checkService: title: "Turn on messages" content: "To receive the signed documents, you must turn on the setting that allows the service to send you messages. If you don't turn it on, you can complete the signature but you won't receive the signed documents." @@ -2836,6 +2930,28 @@ features: signatureFieldInfo: title: "What are restrictive clauses?" subTitle: "These are clauses that highlight specific conditions to be aware of and that must be accepted. By law, they have to be easily identifiable to make sure the signer is aware of their contents before signing." + wallet: + home: + emptyMessage: Custodisci qui i tuoi metodi di pagamento, Carta Giovani Nazionale, bonus e sconti. + cta: Aggiungi al Portafoglio + toast: + newMethod: Elemento aggiunto! + paymentsBanner: + title: Vuoi pagare un avviso? + action: Vai alla sezione Pagamenti + close: Chiudi + cards: + categories: + bonus: Iniziative welfare + cgn: Carta Giovani Nazionale + itw: IT Wallet + payment: Payment methods + onboarding: + title: Cosa vuoi aggiungere al portafoglio? + options: + cgn: Carta Giovani Nazionale + welfare: Iniziative welfare + payments: Metodi di pagamento webView: error: missingParams: Not all information necessary to access this page are available @@ -3402,10 +3518,10 @@ fastLogin: subtitle: For security reasons, you must authenticate again with SPID or CIE. You can then resume IO configuration from where you left off. submitButtonTitle: Continue continueNavigation: - title: "La tua sessione è scaduta" - subtitle: "Per continuare a utilizzare l’app premi su Continua la navigazione." - submitButtonTitle: "Continua la navigazione" - cancelButtonTitle: "Annulla" + title: "Your session has expired" + subtitle: "Please log in to continue using the app" + submitButtonTitle: "Log in" + cancelButtonTitle: "Cancel" transientError: title: "We couldn't authenticate you" subtitle: "Close the app and try again in a few minutes." diff --git a/locales/en/jailbroken_body.md b/locales/en/jailbroken_body.md index 4a755794c22..fc03b935357 100644 --- a/locales/en/jailbroken_body.md +++ b/locales/en/jailbroken_body.md @@ -1,7 +1,7 @@ -It seems like your device has been **jailbroken**. +A **jailbreak** has been performed on your device. This happens when the operating system is modified or incorrectly installed. This could compromise the integrity of the system and the IO application may not function properly. -This happens when the operating system undergoes an unusual modification or installation. The integrity of the operating system may have been compromised and the IO app may not be working properly. +**What should you do?** -If possible, please do not continue and install the IO app on a different device running an operating system which has integrity and is up-to-date. +Use the IO app on a device with an intact and up-to-date operating system. -If you continue, you agree to proceed at your own risk in interacting with the services offered, as lack of integrity may lead to data theft and/or other fraudulent activities. You also agree that the app developer is in no way liable for any damage, loss, or consequence you might incur as a result of using the IO app in this execution context. +By continuing, you agree to continue to interact with the services offered at your own risk. Lack of integrity can lead to data theft or other fraudulent activities. You also agree that the app developer shall not be liable in any way for any damage, loss or consequence caused by the IO app in connection with this performance. diff --git a/locales/en/rooted_body.md b/locales/en/rooted_body.md index 3baa573985c..05dce0aba89 100644 --- a/locales/en/rooted_body.md +++ b/locales/en/rooted_body.md @@ -1,7 +1,7 @@ -It seems like **root** permissions are enabled on your device. +You have **root permissions** active on your device. This happens when the operating system is modified or incorrectly installed. This could compromise the integrity of the system and the IO application may not function properly. -This happens when the operating system undergoes an unusual modification or installation. The integrity of the operating system may have been compromised and the IO app may not be working properly. +**What should you do?** -If possible, please do not continue and install the IO app on a different device running an operating system which has integrity and is up-to-date. +Use the IO app on a device with an intact and up-to-date operating system. -If you continue, you agree to proceed at your own risk in interacting with the services offered, as lack of integrity may lead to data theft and/or other fraudulent activities. You also agree that the app developer is in no way liable for any damage, loss, or consequence you might incur as a result of using the IO app in this execution context. +By continuing, you agree to continue to interact with the services offered at your own risk. Lack of integrity can lead to data theft or other fraudulent activities. You also agree that the app developer shall not be liable in any way for any damage, loss or consequence caused by the IO app in connection with this performance. diff --git a/locales/it/cie/cieNotSupported.md b/locales/it/cie/cieNotSupported.md deleted file mode 100644 index 48b29e48d14..00000000000 --- a/locales/it/cie/cieNotSupported.md +++ /dev/null @@ -1,4 +0,0 @@ -Il dispositivo che stai utilizzando non supporta l’accesso con Carta d’Identità Elettronica (CIE). L’accesso con CIE è supportato da: - -- dispositivi con chip NFC e sistema operativo Android 6.0 o superiore; -- iPhone dal modello 7 in su con sistema operativo iOS 13 o superiore. \ No newline at end of file diff --git a/locales/it/index.yml b/locales/it/index.yml index d55a2f36ae3..63abffdd875 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -190,6 +190,7 @@ global: messages: Messaggi wallet: Portafoglio scan: Inquadra + payments: Pagamenti documents: Documenti profile: Profilo services: Servizi @@ -213,6 +214,11 @@ tablet: message: L’interfaccia di IO è ottimizzata per smartphone, ma tutte le funzioni compatibili con il tuo dispositivo sono comunque disponibili. rooted: title: Sembra che la sicurezza del tuo dispositivo sia compromessa! + body: Se premi su ‘Continua’, accetti di proseguire a tuo rischio nella navigazione, altrimenti chiudi l’app. + learnMoreButton: + title: Cosa significa? + learnMoreBottomsheet: + title: Sicurezza del dispositivo bodyiOS: !include jailbroken_body.md bodyAndroid: !include rooted_body.md continueAlert: @@ -229,6 +235,7 @@ locales: en: Inglese de: Tedesco clipboard: + copyIntoClipboard: copia negli appunti copyFeedback: Copiato negli appunti copyText: copia copyFeedbackButton: copiato @@ -249,7 +256,7 @@ permissionRationale: title: "Consenti l’accesso alla fotocamera" message: "Abbiamo bisogno dell'accesso alla fotocamera per utilizzare la torcia." startup: - title: Inizializzazione + title: Stiamo preparando la tua esperienza su IO authentication: Utente autenticato sessionInfo: Attributi SPID caricati profileInfo: Profilo utente caricato @@ -437,6 +444,7 @@ profile: idpayTest: IDPay idpayTestAlert: La modifica richiede il riavvio dell'app designSystemEnvironment: Design System sperimentale + newWalletSection: Nuova sezione portafoglio appVersion: Versione backendVersion: Versione Backend debugMode: Modalità debug @@ -447,8 +455,9 @@ profile: title: Attenzione message: Per aggiungere una nuova carta di test, devi abilitare l'ambiente pagoPA di test dalle impostazioni. closeButton: Chiudi - walletPlayground: - titleSection: "New Wallet" + itWallet: + itWalletTest: IT Wallet + itWalletTestDescription: Abilita ambiente di test per IT Wallet data: title: I tuoi dati subtitle: Qui trovi i tuoi dati anagrafici e di contatto utilizzati da app IO @@ -560,13 +569,15 @@ authentication: hint: Carosello contenente una guida introduttiva alle funzionalità di IO. Seleziona per consultarne il contenuto session_expired: title: La tua sessione è scaduta - body: Per ragioni di sicurezza, dopo {{days}} giorni dall'ultimo accesso con SPID o CIE, ti chiediamo di inserire nuovamente le credenziali per utilizzare IO. + body: Per ragioni di sicurezza, ti chiediamo di entrare con SPID o CIE ogni {{days}} giorni o se esci dall’app. cie_unsupported: - title: Accesso con CIE non disponibile - body: !include cie/cieNotSupported.md - android_desc: !include cie/cieNotSupported_android.md - os_version_unsupported: un sistema operativo Android 6.0 o superiore - nfc_incompatible: un chip NFC compatibile + title: Non puoi entrare con CIE + body: Il dispositivo che stai utilizzando non supporta l’accesso con Carta d’Identità Elettronica (CIE). + button: Ho capito + nfc_problem: Per leggere la CIE è necessario un dispositivo con chip NFC. + os_problem: È richiesto un sistema operativo Android 6.0 o iOS 13 o superiori. + nfc_alert: Il tuo dispositivo non ha l'NFC. + os_alert: Il sistema operativo del dispositivo non è aggiornato. contentTitleCie: Non hai SPID o la CIE? cie_information_request: !include cie.md spid_or_cie: Per utilizzare IO devi dotarti di uno di questi strumenti digitali, che certificano in maniera certa la tua identità. @@ -579,6 +590,7 @@ authentication: loginSpidCie: Accedi con SPID o CIE infoSpidCie: Introduzione a SPID e CIE loginSpidCieContent: Da oggi, oltre a SPID, puoi usare la tua Carta d'Identità Elettronica per accedere + privacyLink: Informativa privacy nospid: Non hai SPID? Scopri di più nospid-nocie: Non hai SPID o CIE? Scopri di più card1-title: Tutti i messaggi in un solo posto @@ -587,10 +599,8 @@ authentication: card2-content: Puoi pagare un tributo o una multa con il tuo conto corrente e le tue carte card3-title: Paga velocemente un avviso cartaceo card3-content: Tramite la scansione del QR code puoi velocemente pagare anche un avviso cartaceo - card4-title: Puoi accedere solo con SPID - card4-content: Il dispositivo che stai utilizzando non supporta l’accesso con Carta d’Identità Elettronica (CIE) - card5-title: Benvenuto nella prima versione pubblica di IO! - card5-content: In alto a destra trovi l'icona '?' per aprire una chat con il team di IO se qualcosa non funziona come dovrebbe. + card5-title: Ti diamo il benvenuto su IO! + card5-content: Vediamo insieme cosa puoi fare card5-content-accessibility: Seleziona elemento precedente per aprire una chat con il team di IO se qualcosa non funziona come dovrebbe. expiredCardTitle: Carta scaduta o non più valida expiredCardHeaderTitle: Entra con CIE @@ -654,6 +664,7 @@ authentication: cardLocked: Carta CIE bloccata wrongPin1AttemptLeft: PIN errato, hai ancora 1 tentativo wrongPin2AttemptLeft: PIN errato, hai ancora 2 tentativi + genericError: Si è verificato un errore imprevisto. Riprova. error: readerCardLostTitle: Avvicina nuovamente la carta readerCardLostTitleiOS: Hai rimosso la carta troppo presto @@ -708,6 +719,8 @@ authentication: dualRecoverDescription: Se hai dimenticato le tue credenziali, ti consigliamo di procedere cliccando sui bottoni qui di seguito. recoverUsername: Recupera il nome utente recoverPassword: Recupera la password + idp_login_success: + contentTitle: Ciao {{name}}! errors: notoken: La sessione non è pronta o non è valida logout: Errore nel logout @@ -752,10 +765,10 @@ authentication: email: cduScreens: validateMail: - title: Non hai confermato il tuo indirizzo email - subtitle: Per continuare a utilizzare l’app è necessario validare il tuo indirizzo email - editButton: Modifica email - validateButton: Conferma email + title: È questa la tua email? + subtitle: Per continuare a usare l’app, devi confermare che la tua email è + editButton: No, modifica + validateButton: Sì, conferma header: title: Configura IO help: @@ -763,7 +776,7 @@ email: emailAlreadyTaken: title: Modifica la tua email subtitleStart: Il tuo indirizzo email - subtitleEnd: risulta già in uso su IO, è necessario inserirne uno diverso per continuare a utilizzare l’app. + subtitleEnd: è già in uso su IO, devi inserirne uno diverso per continuare a usare l’app. editButton: Modifica email header: title: Configura IO @@ -790,17 +803,20 @@ email: newinsert: header: Configura IO title: Qual è la tua email? - subtitle: Utilizziamo l’email per alcune comunicazioni, come le ricevute di pagamento o i messaggi scambiati con l’assistenza. + subtitle: Servirà a inviarti comunicazioni come ricevute di pagamento o messaggi di assistenza. label: Indirizzo email personale alert: - title: L’email {{email}} associata alla tua identità SPID è già in uso su IO, inseriscine una diversa. - description: "email già in uso" - modaltitle: "Questa email è già in uso" - modaldescription: "Può succedere se condividi lo stesso indirizzo con un familiare" - modalbutton: "Usa un’altra email" + title: "{{email}} è già in uso, inseriscine un’altra." + description1: Email già in uso + description2: Inserisci un indirizzo diverso da quello attuale + description3: Stai già usando questa email + invalidemail: Inserisci un indirizzo email valido + modaltitle: Questa email è già in uso + modaldescription: Può succedere se condividi lo stesso indirizzo con un familiare + modalbutton: Usa un’altra email edit: title: Modifica la tua email - subtitle: L'email che avevi inserito in precedenza è + subtitle: Ora stai usando validated: L'email che avevi inserito e validato in precedenza è label: Nuovo indirizzo email personale cta: Modifica email @@ -820,12 +836,13 @@ email: validated: Email validata! validated_ok: Grazie, ora puoi usare le funzionalità legate ai pagamenti, all’inoltro dei messaggi, etc. newvalidate: - title: Conferma la tua email - subtitle: Segui le istruzioni che abbiamo inviato a - link: Non è corretta? + title: Ti abbiamo inviato un’email + subtitle: Per confermare il tuo indirizzo, segui le istruzioni che abbiamo inviato a + link: Non è corretto? + countdowntext: Richiedi una nuova email tra buttonlabelsent: Email inviata! buttonlabelsentagain: Invia di nuovo l’email - toast: Fatto! Controlla tua casella di posta. + toast: Fatto! Controlla la tua casella di posta. newvalidemail: title: Email confermata! subtitle: Riceverai le comunicazioni di IO all’indirizzo @@ -925,6 +942,18 @@ datetimes: todayAt: oggi, alle notValid: Data non valida. payment: + homeScreen: + title: Pagamenti + methodsSection: + header: Metodi di pagamento + headerCTA: Aggiungi + historySection: + header: Storico operazioni + headerCTA: Vedi tutte + transactionStatusBadges: + failure: "FALLITA" + success: "" + CTA: Paga un avviso alertNoPaymentMethods: title: Aggiungi un metodo per effettuare pagamenti in app message: Il metodo verrà salvato nel Portafoglio, così la prossima volta potrai pagare più facilmente. @@ -1202,10 +1231,11 @@ wallet: title: "Non lo so" description: "Cercheremo tutte le carte intestate a te presso le banche aderenti al servizio" generalError: "C'è stato un errore generico durante l'aggiunta del metodo di pagamento" - success: - title: "La carta è stata aggiunta!" - continueButton: "Vedi dettagli" - failure: + outcome: + SUCCESS: + title: "Il metodo è stato aggiunto" + subtitle: "" + primaryAction: "Vedi dettagli" GENERIC_ERROR: title: Si è verificato un errore imprevisto subtitle: Riprova o contatta l'assistenza @@ -1231,6 +1261,10 @@ wallet: title: Hai già aggiunto questo metodo subtitle: "" primaryAction: Chiudi + BPAY_NOT_FOUND: + title: Non abbiamo trovato alcun BANCOMAT Pay attivo + subtitle: "Contatta la tua Banca per attivare il servizio." + primaryAction: Chiudi alert: supportedCardPageLinkError: Si è verificato un errore durante l'apertura della pagina di riferimento per le carte supportate. msgErrorUpdateApp: "Si è verificato un errore durante l'apertura dello store delle app" @@ -1740,12 +1774,19 @@ wallet: subtitle: Per maggiori informazioni, contatta la tua banca. CANCELED_BY_USER: title: L’operazione è stata annullata - subtitle: Non è stato addebitato alcun importo. + subtitle: Se vuoi pagare l’avviso, attendi qualche minuto prima di riprovare. EXCESSIVE_AMOUNT: title: Autorizzazione negata subtitle: Non è stato addebitato alcun importo.\nProbabilmente hai sforato i limiti di spesa previsti dalla tua banca. INVALID_METHOD: title: Il metodo di pagamento non è supportato + WAITING_CONFIRMATION_EMAIL: + title: Attendi la conferma via email + subtitle: Riceverai l'esito del pagamento all'indirizzo {{email}} + defaultSubtitle: Riceverai l'esito del pagamento all'indirizzo email associato. + METHOD_NOT_ENABLED: + title: Abilita il metodo per effettuare pagamenti in app + subtitle: Prima di riprovare con il pagamento, seleziona il metodo nel tuo Portafoglio e abilita la funzionalità “Pagamenti in app”. support: button: "Contatta l'assistenza" supportTitle: Contatta l'assistenza @@ -1879,6 +1920,7 @@ messages: success: "Ripristino effettuato." failure: "Il ripristino non è andato a buon fine" messageDetails: + accessibilityAttachmentIcon: "Contiene allegati" contextualHelpTitle: Dettagli del messaggio contextualHelpContent: !include messages/message_detail.md headerTitle: Messaggio @@ -1920,12 +1962,30 @@ messageDetails: bottomSheet: title: "Cos'è un messaggio dinamico?" body: "È un messaggio che l'ente può modificare dopo averlo inviato. In questo modo, le informazioni che contiene saranno sempre corrette.\n\nUn ente può modificare le informazioni di un messaggio dinamico solo in casi specifici, per esempio per cambiare un'informazione vecchia o aggiornare un allegato non più valido.\n\nSe deve comunicarti nuove informazioni o aggiornamenti importanti, ti invierà un nuovo messaggio." + footer: + contacts: "Contatta il mittente" + showMoreData: "Mostra altri dati" + showMoreDataBottomSheet: + title: "Dati identificativi" + messageId: "ID messaggio" + messageIdAccessibility: "ID messaggio, copia" + contactsBottomSheet: + title: "Assistenza" + body: "Se non ti è chiaro il contenuto del messaggio, contatta l’ente che te lo ha inviato." + actions: + call: Fai una chiamata + email: Scrivi un'email messagePDFPreview: title: "Anteprima PDF" singleBtn: "Salva o condividi" + singleBtnAccessibility: "Salva o condividi documento" open: "Apri" save: "Salva" + saveAccessibility: "Salva documento" savedAtLocation: "Il documento {{name}} è stato salvato nella cartella Download." + share: "Condividi" + shareAccessibility: "Condividi documento" + pdfAccessibility: "Anteprima documento PDF" errors: previewing: title: "L’anteprima non è disponibile" @@ -1975,6 +2035,7 @@ services: value: "Configurazione rapida" label: "tutti i servizi che non hai disattivato possono contattarti." title: "Usa configurazione rapida" + successAlert: "Hai abilitato la configurazione rapida" body: text1: "Ti contatteranno solo gli enti che hanno qualcosa di importante da dirti." text2: "Potrai sempre disattivare le comunicazioni dei servizi che non ti interessano." @@ -1982,6 +2043,7 @@ services: value: "Configurazione manuale" label: "ti possono contattare solo i servizi che hai attivato." title: "Configura manualmente" + successAlert: "Hai abilitato la configurazione manuale" body: text1: "Nessuno potrà contattarti. Per ricevere messaggi dovrai attivare i servizi uno a uno." bottomSheet: @@ -2023,6 +2085,17 @@ services: disableAllMsg: "Ricorda che su IO ti scriveranno solo i servizi che effettivamente hanno qualche informazione personalizzata da comunicarti. Non riceverai messaggi di spam. Se disattivi tutti i servizi, questi non potranno più contattarti tramite IO (finché non li riattiverai). Potrai continuare a fruire i servizi attraverso altri canali (sportello, sito, etc.)" close: Chiudi emptyListMessage: Non ci sono servizi disponibili al momento, trascina in basso per aggiornare + details: + failure: + title: "C'è un problema temporaneo, riprova" + subtitle: "Stiamo recuperando i dettagli del servizio" + back: "Indietro" + retry: "Riprova" + preferences: + title: "Questo servizio può" + inbox: "Contattarti in app" + pushNotifications: "Inviarti notifiche push" + messageReadStatus: "Ricevere conferme di lettura" serviceDetail: fiscalCode: "C.F. Ente" fiscalCodeAccessibility: "Codice fiscale Ente" @@ -2041,16 +2114,21 @@ serviceDetail: disabled: disabilitato notValidated: Per poter abilitare le notifiche via email devi validare il tuo indirizzo email." identification: - title: Ciao! + instructions: + unlockCode: codice di sblocco + unlockCodepPrefix: inserisci il + fingerprint: impronta + fingerprintPrefix: usa l' + faceId: volto + faceIdPrefix: usa il + congiunction: o + title: Ciao {{name}}! titleProfileName: Ciao {{profileName}}! titleValidation: Conferma l'operazione logout: Esci logoutProfileName: Non sei {{profileName}}? logoutDescription: Puoi accedere con le tue credenziali SPID o CIE. Questa sessione verrà disconnessa. logoutDescriptionProfileName: Puoi accedere con le tue credenziali SPID o CIE. La sessione di {{profileName}} verrà disconnessa. - subtitleCode: Inserisci il codice di sblocco - subtitleCodeFingerprint: "usa l'impronta o il codice di sblocco" - subtitleCodeFaceId: "usa Face ID o il codice di sblocco" biometric: fingerprintType: Impronta digitale title: Identificazione Biometrica @@ -2059,9 +2137,9 @@ identification: title: Identificazione richiesta sensorDescription: Accedi velocemente fail: - remainingAttempts: Ti rimangono {{attempts}} tentativi. - remainingAttemptSingle: Ti rimane {{attempts}} tentativo. - tooManyAttempts: Troppi tentativi di inserimento errati. + remainingAttempts: Hai a disposizione ancora {{attempts}} tentativi + remainingAttemptSingle: Hai a disposizone ancora {{attempts}} tentativo + tooManyAttempts: Hai inserito troppe volte il codice sbagliato! waitMessage: "Riprova tra:" unlockCode: accessibility: @@ -2544,7 +2622,7 @@ bonus: title: "Stiamo processando la tua richiesta." body: "Ti invieremo un messaggio in app quando la tua Carta Giovani sarà attiva." alreadyActive: - title: "La tua Carta Giovani Nazionale è attiva!" + title: "La tua Carta Giovani Nazionale è già attiva!" body: "Puoi visualizzarne i dettagli nella sezione Portafoglio." pending: title: "La tua Carta Giovani Nazionale è in fase di attivazione." @@ -2620,12 +2698,24 @@ bonus: conjunction: ", " features: messages: + attachmentDownloadFeedback: "Download in corso" + attachments: "Allegati" loading: title: "Sto caricando i dettagli del messaggio" subtitle: "Attendi qualche secondo" updateBottomSheet: title: "Aggiorna l'app IO" subtitle: "Per usare questa funzionalità è richiesta la versione {{value}} di app IO. Aggiorna l’app per continuare." + badge: + dueDate: Scaduto il {{date}} alle ore {{time}} + alert: + addReminder: Aggiungi promemoria + removeReminder: Rimuovi promemoria + content: Scade il {{date}} alle ore {{time}} + payments: + title: "Avvisi pagoPA" + noticeCode: "Codice avviso" + pay: "Paga" euCovidCertificate: save: album: "IO" @@ -2778,6 +2868,10 @@ features: content: "Vuoi annullare la firma di **{{dossierTitle}}**?" cancel: "Annulla la firma" confirm: "Continua a firmare" + alert: + title: "Vuoi interrompere l'operazione?" + confirm: "Sì, interrompi" + cancel: "No, torna indietro" checkService: title: "Attiva i messaggi" content: "Per ricevere i documenti firmati devi attivare l’opzione che consente al servizio di inviarti messaggi. Se non la attivi, puoi completare la firma ma non riceverai i documenti firmati." @@ -2836,6 +2930,28 @@ features: signatureFieldInfo: title: "Cosa sono le clausole vessatorie?" subTitle: "Sono delle clausole che segnalano condizioni particolari a cui prestare attenzione e che devono essere accettate. Devono inoltre essere, per legge, facilmente identificabili per far sì che chi firma sia informato del loro contenuto prima di apporre la firma." + wallet: + home: + emptyMessage: Custodisci qui i tuoi metodi di pagamento, Carta Giovani Nazionale, bonus e sconti. + cta: Aggiungi al Portafoglio + toast: + newMethod: Elemento aggiunto! + paymentsBanner: + title: Vuoi pagare un avviso? + action: Vai alla sezione Pagamenti + close: Chiudi + cards: + categories: + bonus: Iniziative welfare + cgn: Carta Giovani Nazionale + itw: IT Wallet + payment: Metodi di pagamento + onboarding: + title: Cosa vuoi aggiungere al portafoglio? + options: + cgn: Carta Giovani Nazionale + welfare: Iniziative welfare + payments: Metodi di pagamento webView: error: missingParams: Non sono presenti le informazioni necessarie per accedere a questa pagina @@ -3399,12 +3515,12 @@ fastLogin: sessionExpired: noPin: title: È passato troppo tempo! - subtitle: Per ragioni di sicurezza devi autenticarti di nuovo con SPID o CIE. Potrai poi continuare la configurazione di IO da dove l’avevi interrotta. + subtitle: Per motivi di sicurezza, devi identificarti di nuovo con SPID o CIE. Potrai poi riprendere la configurazione di IO da dove l’avevi interrotta. submitButtonTitle: Continua continueNavigation: title: "La tua sessione è scaduta" - subtitle: "Per continuare a utilizzare l’app premi su Continua la navigazione." - submitButtonTitle: "Continua la navigazione" + subtitle: "Accedi per continuare a usare l’app." + submitButtonTitle: "Accedi" cancelButtonTitle: "Annulla" transientError: title: "Non siamo riusciti ad autenticarti" diff --git a/locales/it/jailbroken_body.md b/locales/it/jailbroken_body.md index 6e155604224..9762448e8c1 100644 --- a/locales/it/jailbroken_body.md +++ b/locales/it/jailbroken_body.md @@ -1,7 +1,7 @@ -Sembra che sul tuo dispositivo sia stata effettuata la procedura di **jailbreak**. +Sul tuo dispositivo è stato effettuato un **jailbrack**. Questo succede quando il sistema operativo viene modificato o installato in modo anomalo. Questo potrebbe compromettere l’integrità del sistema e l'app IO potrebbe non funzionare correttamente. -Questo accade quando il sistema operativo viene modificato oppure quando viene installato in modo anomalo. L'integrità del sistema operativo potrebbe essere stata compromessa e l'app IO potrebbe non funzionare correttamente. +**Cosa fare?** -Se possibile, ti invitiamo a non continuare e ad installare l'app IO su un dispositivo diverso che abbia un sistema operativo integro e aggiornato. +Ti invitiamo a usare l'app IO su un dispositivo diverso, con un sistema operativo integro e aggiornato. -Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti, poiché la mancanza di integrità può condurre al furto di dati e/o ad altre attività fraudolente. Accetti inoltre che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze da te subite in virtù dell'utilizzo dell'app IO in questo contesto di esecuzione. +Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti. La mancanza di integrità può portare al furto di dati o ad altre attività fraudolente. Accetti anche che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze causate dall'app IO in questo contesto di esecuzione. \ No newline at end of file diff --git a/locales/it/rooted_body.md b/locales/it/rooted_body.md index 48b700b5e79..f22ce4f6fbf 100644 --- a/locales/it/rooted_body.md +++ b/locales/it/rooted_body.md @@ -1,7 +1,7 @@ -Sembra che sul tuo dispositivo siano abilitati i permessi di **root**. +Sul tuo dispositivo sono attivi i **permessi di root**. Questo succede quando il sistema operativo viene modificato o installato in modo anomalo. Questo potrebbe compromettere l’integrità del sistema e l'app IO potrebbe non funzionare correttamente. -Questo accade quando il sistema operativo viene modificato oppure quando viene installato in modo anomalo. L'integrità del sistema operativo potrebbe essere stata compromessa e l'app IO potrebbe non funzionare correttamente. +**Cosa fare?** -Se possibile, ti invitiamo a non continuare e ad installare l'app IO su un dispositivo diverso che abbia un sistema operativo integro e aggiornato. +Ti invitiamo a usare l'app IO su un dispositivo diverso, con un sistema operativo integro e aggiornato. -Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti, poiché la mancanza di integrità può condurre al furto di dati e/o ad altre attività fraudolente. Accetti inoltre che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze da te subite in virtù dell'utilizzo dell'app IO in questo contesto di esecuzione. +Se continui, accetti di proseguire a tuo rischio nell'interazione con i servizi offerti. La mancanza di integrità può portare al furto di dati o ad altre attività fraudolente. Accetti anche che lo sviluppatore dell’app non sia in alcun modo responsabile di eventuali danni, perdite o conseguenze causate dall'app IO in questo contesto di esecuzione. \ No newline at end of file diff --git a/package.json b/package.json index ac533d5a0ed..e97c41709af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "italia-app", - "version": "2.53.0-rc.0", + "version": "2.58.0-rc.0", "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/api_backend.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/api_public.yaml", "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.30/definitions.yml", @@ -19,8 +19,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.32.1-RELEASE/api_lollipop_first_consumer.yaml", "fast_login_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/openapi/generated/api_fast_login.yaml", - "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/a5299db39de86a951d353fdc2bfed48188c0c125/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", + "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/bec0f57606a2002706bd5f5286fad3e508d2f50e/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", + "pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/2adee4c0a8de8570e74710379f9cccb04c6925b6/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", "private": true, "scripts": { "start": "react-native start", @@ -34,7 +34,7 @@ "start-breaking-cycle": "yarn pre-cycle && standard-version -t \"\" --prerelease rc --release-as major --no-verify && yarn push-hint", "attach": "react-native attach", "postinstall": "patch-package && rn-nodeify --install path,buffer && chmod +x ./bin/add-ios-source-maps.sh && ./bin/add-ios-source-maps.sh && chmod +x ./bin/add-ios-env-config.sh && ./bin/add-ios-env-config.sh", - "test:ci": "jest --ci --maxWorkers=2 --silent", + "test:ci": "jest --ci --maxWorkers=4 --silent", "test:dev": "jest --detectOpenHandles --coverage=false", "test:tz": "yarn test:tz-eu-rome && yarn test:tz-us-ny && yarn test:tz-us-yt && yarn test:tz-au-syd && yarn test:tz-eu-rome-comp", "test:tz-eu-rome": "TZ='Europe/Rome' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date IT' './ts/utils/__tests__/fiscal-code.test.ts'", @@ -76,9 +76,9 @@ "generate:idpay": "npm-run-all generate:idpay-api", "generate:lollipop-api": "rimraf definitions/lollipop && mkdir -p definitions/lollipop && gen-api-models --api-spec $npm_package_lollipop_api --out-dir ./definitions/lollipop --no-strict --response-decoders --request-types --client", "generate:fast-login-api": "rimraf definitions/fast_login && mkdir -p definitions/fast_login && gen-api-models --api-spec $npm_package_fast_login_api --out-dir ./definitions/fast_login --no-strict --response-decoders --request-types --client", - "generate:wallet-api": "rimraf definitions/pagopa/walletv3 && mkdir -p definitions/pagopa/walletv3 && gen-api-models --api-spec $npm_package_pagopa_api_walletv3 --out-dir ./definitions/pagopa/walletv3 --no-strict --response-decoders --request-types --client", + "generate:pagopa-walletv3-api": "rimraf definitions/pagopa/walletv3 && mkdir -p definitions/pagopa/walletv3 && gen-api-models --api-spec $npm_package_pagopa_api_walletv3 --out-dir ./definitions/pagopa/walletv3 --no-strict --response-decoders --request-types --client", "generate:pagopa-ecommerce-api": "rimraf definitions/pagopa/ecommerce && mkdir -p definitions/pagopa/ecommerce && gen-api-models --api-spec $npm_package_pagopa_api_ecommerce --out-dir ./definitions/pagopa/ecommerce --no-strict --response-decoders --request-types --client", - "generate:wallet": "npm-run-all generate:wallet-api", + "generate:payments": "npm-run-all generate:pagopa-walletv3-api generate:pagopa-ecommerce-api", "generate": "npm-run-all generate:*", "locales_unused": "ts-node --skip-project -O '{\"lib\":[\"es2015\"]}' scripts/unused-locales.ts", "remove_unused_locales": "ts-node --skip-project -O '{\"lib\":[\"es2015\"]}' scripts/remove-unused-locales.ts", @@ -95,25 +95,25 @@ "dependencies": { "@babel/plugin-transform-regenerator": "^7.18.6", "@gorhom/bottom-sheet": "^4.1.5", - "@pagopa/io-app-design-system": "1.20.1", + "@pagopa/io-app-design-system": "1.33.0", "@pagopa/io-pagopa-commons": "^3.1.0", - "@pagopa/io-react-native-crypto": "^0.2.1", - "@pagopa/io-react-native-login-utils": "^0.2.2", - "@pagopa/io-react-native-zendesk": "^0.3.28", - "@pagopa/react-native-cie": "1.2.0", + "@pagopa/io-react-native-crypto": "^0.3.0", + "@pagopa/io-react-native-http-client": "^0.1.3", + "@pagopa/io-react-native-login-utils": "^1.0.0", + "@pagopa/io-react-native-zendesk": "^0.3.29", + "@pagopa/react-native-cie": "^1.2.1", "@pagopa/ts-commons": "^10.15.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-camera-roll/camera-roll": "5.6.1", "@react-native-clipboard/clipboard": "^1.10.0", - "@react-native-community/datetimepicker": "^3.5.2", "@react-native-community/push-notification-ios": "^1.8.0", "@react-native-community/slider": "^3.0.3", "@react-native-cookies/cookies": "^6.2.1", - "@react-native-picker/picker": "^2.4.1", - "@react-navigation/bottom-tabs": "^5.11.15", - "@react-navigation/material-top-tabs": "^5.x", - "@react-navigation/native": "^5.9.8", - "@react-navigation/stack": "^5.14.9", + "@react-navigation/bottom-tabs": "6.5.11", + "@react-navigation/material-top-tabs": "6.6.5", + "@react-navigation/native": "6.1.9", + "@react-navigation/native-stack": "^6.9.17", + "@react-navigation/stack": "6.3.20", "@redux-saga/testing-utils": "^1.1.3", "@xstate/react": "^3.0.1", "async-mutex": "^0.1.3", @@ -133,13 +133,14 @@ "jwk-thumbprint": "^0.1.4", "lodash": "^4.17.21", "metro-babel-register": "^0.72.1", + "mixpanel-react-native": "2.4.1", "native-base": "^2.15.2", "native-base-shoutem-theme": "0.3.1", "pako": "^2.1.0", "path-browserify": "0.0.0", "pdf-lib": "^1.17.1", - "react": "18.0.0", - "react-native": "0.69.9", + "react": "18.1.0", + "react-native": "0.70.15", "react-native-android-open-settings": "^1.3.0", "react-native-background-timer": "2.1.1", "react-native-barcode-builder": "^2.0.0", @@ -148,13 +149,13 @@ "react-native-config": "^1.4.5", "react-native-crypto": "^2.1.0", "react-native-device-info": "^10.8.0", - "react-native-document-picker": "^9.0.1", + "react-native-document-picker": "^9.1.1", "react-native-exception-handler": "^2.10.8", "react-native-fingerprint-scanner": "^6.0.0", "react-native-flag-secure-android": "^1.0.3", "react-native-flipper": "^0.154.0", "react-native-fs": "^2.18.0", - "react-native-gesture-handler": "^2.12.0", + "react-native-gesture-handler": "^2.12.1", "react-native-haptic-feedback": "^2.0.2", "react-native-i18n": "^2.0.15", "react-native-image-pan-zoom": "^2.1.11", @@ -163,8 +164,7 @@ "react-native-keychain": "^4.0.5", "react-native-linear-gradient": "^2.5.6", "react-native-masked-text": "^1.13.0", - "react-native-mixpanel": "1.2.0", - "react-native-modal-datetime-picker": "^10.2.0", + "react-native-pager-view": "^6.2.3", "react-native-pdf": "^6.4.0", "react-native-pdf-thumbnail": "^1.2.1", "react-native-permissions": "^4.0.0", @@ -176,12 +176,12 @@ "react-native-responsive-screen": "^1.4.1", "react-native-safe-area-context": "^3.3.2", "react-native-screen-brightness": "^2.0.0-alpha", - "react-native-screens": "^2.18.1", + "react-native-screens": "^3.30.1", "react-native-sha256": "1.2.3", "react-native-share": "7.3.9", "react-native-splash-screen": "^3.2.0", - "react-native-svg": "^12.3.0", - "react-native-tab-view": "^2.x", + "react-native-svg": "^15.1.0", + "react-native-tab-view": "3.5.2", "react-native-view-shot": "3.1.2", "react-native-vision-camera": "2.15.4", "react-native-webview": "^11.26.1", @@ -218,7 +218,7 @@ "xstate": "^4.33.6" }, "devDependencies": { - "@babel/core": "^7.15.0", + "@babel/core": "^7.18.8", "@babel/preset-typescript": "^7.16.7", "@babel/runtime": "^7.15.3", "@jambit/eslint-plugin-typed-redux-saga": "^0.4.0", @@ -238,7 +238,7 @@ "@types/pako": "^2.0.0", "@types/prettier": "^2.7.3", "@types/react": "16.9.43", - "@types/react-native": "0.69.6", + "@types/react-native": "0.70.19", "@types/react-native-background-timer": "^2.0.0", "@types/react-native-i18n": "^2.0.0", "@types/react-native-push-notification": "^8.1.1", @@ -275,7 +275,7 @@ "js-yaml": "^3.13.1", "jsdoc-to-markdown": "^6.0.1", "lint-staged": "^13.2.0", - "metro-react-native-babel-preset": "^0.70.3", + "metro-react-native-babel-preset": "^0.73.0", "mockdate": "^3.0.2", "mockttp": "2.4.0", "node-fetch": "^2.6.7", @@ -287,7 +287,7 @@ "react-native-bundle-visualizer": "^2.2.1", "react-native-get-random-values": "^1.7.0", "react-native-svg-transformer": "^0.14.3", - "react-test-renderer": "18.0.0", + "react-test-renderer": "18.1.0", "redux-mock-store": "^1.5.4", "redux-saga-test-plan": "4.0.3", "rn-nodeify": "^10.0.1", diff --git a/patches/@react-navigation+material-top-tabs+5.3.19.patch b/patches/@react-navigation+material-top-tabs+5.3.19.patch deleted file mode 100644 index e3ced5a3cc6..00000000000 --- a/patches/@react-navigation+material-top-tabs+5.3.19.patch +++ /dev/null @@ -1,54 +0,0 @@ -diff --git a/node_modules/@react-navigation/material-top-tabs/.DS_Store b/node_modules/@react-navigation/material-top-tabs/.DS_Store -new file mode 100644 -index 0000000..5172429 -Binary files /dev/null and b/node_modules/@react-navigation/material-top-tabs/.DS_Store differ -diff --git a/node_modules/@react-navigation/material-top-tabs/lib/typescript/.DS_Store b/node_modules/@react-navigation/material-top-tabs/lib/typescript/.DS_Store -new file mode 100644 -index 0000000..5127bf9 -Binary files /dev/null and b/node_modules/@react-navigation/material-top-tabs/lib/typescript/.DS_Store differ -diff --git a/node_modules/@react-navigation/material-top-tabs/lib/typescript/src/types.d.ts b/node_modules/@react-navigation/material-top-tabs/lib/typescript/src/types.d.ts -index 4a4b081..1cf21c0 100644 ---- a/node_modules/@react-navigation/material-top-tabs/lib/typescript/src/types.d.ts -+++ b/node_modules/@react-navigation/material-top-tabs/lib/typescript/src/types.d.ts -@@ -76,6 +76,18 @@ export declare type MaterialTopTabNavigationConfig = Partial['renderPager']; -+ /** -+ * Whether this screen should be lazily rendered. When this is set to `true`, -+ * the screen will be rendered as it comes into the viewport. -+ * By default all screens are rendered to provide a smoother swipe experience. -+ * But you might want to defer the rendering of screens out of the viewport until the user sees them. -+ * To enable lazy rendering for this screen, set `lazy` to `true`. -+ * -+ * When you enable `lazy`, the lazy loaded screens will usually take some time to render -+ * when they come into the viewport. You can use the `lazyPlaceholder` prop to customize -+ * what the user sees during this short period. -+ */ -+ lazy?: boolean; - /** - * Function that returns a React element to render for routes that haven't been rendered yet. - * Receives an object containing the route as the prop. -diff --git a/node_modules/@react-navigation/material-top-tabs/src/types.tsx b/node_modules/@react-navigation/material-top-tabs/src/types.tsx -index 60270ff..d7ae0e8 100644 ---- a/node_modules/@react-navigation/material-top-tabs/src/types.tsx -+++ b/node_modules/@react-navigation/material-top-tabs/src/types.tsx -@@ -121,6 +121,18 @@ export type MaterialTopTabNavigationConfig = Partial< - * The pager handles swipe gestures and page switching. - */ - pager?: React.ComponentProps['renderPager']; -+ /** -+ * Whether this screen should be lazily rendered. When this is set to `true`, -+ * the screen will be rendered as it comes into the viewport. -+ * By default all screens are rendered to provide a smoother swipe experience. -+ * But you might want to defer the rendering of screens out of the viewport until the user sees them. -+ * To enable lazy rendering for this screen, set `lazy` to `true`. -+ * -+ * When you enable `lazy`, the lazy loaded screens will usually take some time to render -+ * when they come into the viewport. You can use the `lazyPlaceholder` prop to customize -+ * what the user sees during this short period. -+ */ -+ lazy?: boolean; - /** - * Function that returns a React element to render for routes that haven't been rendered yet. - * Receives an object containing the route as the prop. diff --git a/patches/@types+react-native+0.69.6.patch b/patches/@types+react-native+0.70.19.patch similarity index 51% rename from patches/@types+react-native+0.69.6.patch rename to patches/@types+react-native+0.70.19.patch index 6cb8cb885df..ea89767f364 100644 --- a/patches/@types+react-native+0.69.6.patch +++ b/patches/@types+react-native+0.70.19.patch @@ -1,21 +1,11 @@ diff --git a/node_modules/@types/react-native/index.d.ts b/node_modules/@types/react-native/index.d.ts -index bf4b3d6..a54a72b 100755 +index 12831dd..aedaba3 100644 --- a/node_modules/@types/react-native/index.d.ts +++ b/node_modules/@types/react-native/index.d.ts -@@ -7723,7 +7723,8 @@ export type Permission = - | 'android.permission.WRITE_EXTERNAL_STORAGE' - | 'android.permission.BLUETOOTH_CONNECT' - | 'android.permission.BLUETOOTH_SCAN' -- | 'android.permission.BLUETOOTH_ADVERTISE'; -+ | 'android.permission.BLUETOOTH_ADVERTISE' -+ | 'android.permission.POST_NOTIFICATIONS'; - - export type PermissionStatus = 'granted' | 'denied' | 'never_ask_again'; - -@@ -8370,6 +8371,10 @@ export interface UIManagerStatic { +@@ -8548,6 +8548,10 @@ export interface UIManagerStatic { * commandArgs - Args of the native method that we can pass from JS to native. */ - dispatchViewManagerCommand: (reactTag: number | null, commandID: number | string, commandArgs?: Array) => void; + dispatchViewManagerCommand: (reactTag: number | null, commandID: number | string, commandArgs?: any[]) => void; + + //Added missing accesibility definition + sendAccessibilityEvent(reactTag?: number, eventType: number): void; diff --git a/patches/native-base+2.15.2.patch b/patches/native-base+2.15.2.patch new file mode 100644 index 00000000000..3fcf5f30b79 --- /dev/null +++ b/patches/native-base+2.15.2.patch @@ -0,0 +1,31 @@ +diff --git a/node_modules/native-base/dist/src/basic/ToastContainer.js b/node_modules/native-base/dist/src/basic/ToastContainer.js +index 832b3e8..d964156 100644 +--- a/node_modules/native-base/dist/src/basic/ToastContainer.js ++++ b/node_modules/native-base/dist/src/basic/ToastContainer.js +@@ -1,2 +1,2 @@ +-Object.defineProperty(exports,"__esModule",{value:true});exports.ToastContainer=undefined;var _extends=Object.assign||function(target){for(var i=1;i=0)continue;if(!Object.prototype.hasOwnProperty.call(obj,i))continue;target[i]=obj[i];}return target;}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call&&(typeof call==="object"||typeof call==="function")?call:self;}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass;}var POSITION={ABSOLUTE:'absolute',BOTTOM:'bottom',TOP:'top'};var ToastContainer=function(_Component){_inherits(ToastContainer,_Component);_createClass(ToastContainer,null,[{key:'show',value:function show(_ref){var config=_objectWithoutProperties(_ref,[]);this.toastInstance._root.showToast({config:config});}},{key:'hide',value:function hide(){if(this.toastInstance._root.getModalState()){this.toastInstance._root.closeToast('functionCall');}}}]);function ToastContainer(props){_classCallCheck(this,ToastContainer);var _this=_possibleConstructorReturn(this,(ToastContainer.__proto__||Object.getPrototypeOf(ToastContainer)).call(this,props));_this.closeModal=function(reason){_this.setState({modalVisible:false});var onClose=_this.state.onClose;if(onClose&&typeof onClose==='function'){onClose(reason);}};_this.state={fadeAnim:new _reactNative.Animated.Value(0),pan:new _reactNative.Animated.ValueXY({x:0,y:0}),keyboardHeight:0,isKeyboardVisible:false,modalVisible:false};_this.keyboardDidHide=_this.keyboardDidHide.bind(_this);_this.keyboardDidShow=_this.keyboardDidShow.bind(_this);_this._panResponder=_reactNative.PanResponder.create({onMoveShouldSetPanResponderCapture:function onMoveShouldSetPanResponderCapture(){return true;},onPanResponderRelease:function onPanResponderRelease(evt,_ref2){var dx=_ref2.dx;if(dx!==0){_reactNative.Animated.timing(_this.state.pan,{toValue:{x:dx,y:0},duration:100,useNativeDriver:false}).start(function(){return _this.closeToast('swipe');});}}});return _this;}_createClass(ToastContainer,[{key:'componentDidMount',value:function componentDidMount(){_reactNative.Keyboard.addListener('keyboardDidShow',this.keyboardDidShow);_reactNative.Keyboard.addListener('keyboardDidHide',this.keyboardDidHide);}},{key:'componentWillUnmount',value:function componentWillUnmount(){_reactNative.Keyboard.removeListener('keyboardDidShow',this.keyboardDidShow);_reactNative.Keyboard.removeListener('keyboardDidHide',this.keyboardDidHide);}},{key:'getToastStyle',value:function getToastStyle(){return{position:POSITION.ABSOLUTE,opacity:this.state.fadeAnim,width:'100%',elevation:9,paddingHorizontal:_reactNative.Platform.OS===_commonColor.PLATFORM.IOS?20:0,top:this.state.position===POSITION.TOP?30:undefined,bottom:this.state.position===POSITION.BOTTOM?this.getTop():undefined};}},{key:'getTop',value:function getTop(){if(_reactNative.Platform.OS===_commonColor.PLATFORM.IOS){if(this.state.isKeyboardVisible){return this.state.keyboardHeight;}return 30;}return 0;}},{key:'getButtonText',value:function getButtonText(buttonText){if(buttonText){if(buttonText.trim().length===0){return undefined;}return buttonText;}return undefined;}},{key:'getModalState',value:function getModalState(){return this.state.modalVisible;}},{key:'keyboardDidHide',value:function keyboardDidHide(){this.setState({keyboardHeight:0,isKeyboardVisible:false});}},{key:'keyboardDidShow',value:function keyboardDidShow(e){this.setState({keyboardHeight:e.endCoordinates.height,isKeyboardVisible:true});}},{key:'showToast',value:function showToast(_ref3){var config=_ref3.config;this.setState({modalVisible:true,text:config.text,buttonText:this.getButtonText(config.buttonText),type:config.type,position:config.position?config.position:POSITION.BOTTOM,supportedOrientations:config.supportedOrientations,style:config.style,buttonTextStyle:config.buttonTextStyle,buttonStyle:config.buttonStyle,textStyle:config.textStyle,onClose:config.onClose,swipeDisabled:config.swipeDisabled||false});if(this.closeTimeout){clearTimeout(this.closeTimeout);}if(config.duration!==0){var duration=config.duration>0?config.duration:1500;this.closeTimeout=setTimeout(this.closeToast.bind(this,'timeout'),duration);}_reactNative.Animated.timing(this.state.fadeAnim,{toValue:1,duration:200,useNativeDriver:false}).start();}},{key:'closeToast',value:function closeToast(reason){var _this2=this;clearTimeout(this.closeTimeout);_reactNative.Animated.timing(this.state.fadeAnim,{toValue:0,duration:200,useNativeDriver:false}).start(function(){_this2.closeModal(reason);_this2.state.pan.setValue({x:0,y:0});});}},{key:'render',value:function render(){var _this3=this;if(this.state.modalVisible){var _state$pan=this.state.pan,x=_state$pan.x,y=_state$pan.y;return _react2.default.createElement(_reactNative.Animated.View,_extends({},this.state.swipeDisabled?{}:this._panResponder.panHandlers,{style:[this.getToastStyle(),{transform:[{translateX:x},{translateY:y}]}],__source:{fileName:_jsxFileName,lineNumber:182}}),_react2.default.createElement(_Toast.Toast,{style:[this.state.style],danger:this.state.type==='danger',success:this.state.type==='success',warning:this.state.type==='warning',__source:{fileName:_jsxFileName,lineNumber:189}},_react2.default.createElement(_Text.Text,{style:this.state.textStyle,__source:{fileName:_jsxFileName,lineNumber:195}},this.state.text),this.state.buttonText&&_react2.default.createElement(_Button.Button,{style:this.state.buttonStyle,onPress:function onPress(){return _this3.closeToast('user');},__source:{fileName:_jsxFileName,lineNumber:197}},_react2.default.createElement(_Text.Text,{style:this.state.buttonTextStyle,__source:{fileName:_jsxFileName,lineNumber:201}},this.state.buttonText))));}return null;}}]);return ToastContainer;}(_react.Component);ToastContainer.propTypes=_extends({},_reactNative.ViewPropTypes);var StyledToastContainer=(0,_nativeBaseShoutemTheme.connectStyle)('NativeBase.ToastContainer',{},_mapPropsToStyleNames2.default)(ToastContainer);exports.ToastContainer=StyledToastContainer; ++Object.defineProperty(exports,"__esModule",{value:true});exports.ToastContainer=undefined;var _extends=Object.assign||function(target){for(var i=1;i=0)continue;if(!Object.prototype.hasOwnProperty.call(obj,i))continue;target[i]=obj[i];}return target;}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call&&(typeof call==="object"||typeof call==="function")?call:self;}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass;}var POSITION={ABSOLUTE:'absolute',BOTTOM:'bottom',TOP:'top'};var ToastContainer=function(_Component){_inherits(ToastContainer,_Component);_createClass(ToastContainer,null,[{key:'show',value:function show(_ref){var config=_objectWithoutProperties(_ref,[]);this.toastInstance._root.showToast({config:config});}},{key:'hide',value:function hide(){if(this.toastInstance._root.getModalState()){this.toastInstance._root.closeToast('functionCall');}}}]);function ToastContainer(props){_classCallCheck(this,ToastContainer);var _this=_possibleConstructorReturn(this,(ToastContainer.__proto__||Object.getPrototypeOf(ToastContainer)).call(this,props));_this.closeModal=function(reason){_this.setState({modalVisible:false});var onClose=_this.state.onClose;if(onClose&&typeof onClose==='function'){onClose(reason);}};_this.state={fadeAnim:new _reactNative.Animated.Value(0),pan:new _reactNative.Animated.ValueXY({x:0,y:0}),keyboardHeight:0,isKeyboardVisible:false,modalVisible:false};_this.keyboardDidHide=_this.keyboardDidHide.bind(_this);_this.keyboardDidShow=_this.keyboardDidShow.bind(_this);_this._panResponder=_reactNative.PanResponder.create({onMoveShouldSetPanResponderCapture:function onMoveShouldSetPanResponderCapture(){return true;},onPanResponderRelease:function onPanResponderRelease(evt,_ref2){var dx=_ref2.dx;if(dx!==0){_reactNative.Animated.timing(_this.state.pan,{toValue:{x:dx,y:0},duration:100,useNativeDriver:false}).start(function(){return _this.closeToast('swipe');});}}});return _this;}_createClass(ToastContainer,[{key:'componentDidMount',value:function componentDidMount(){}},{key:'componentWillUnmount',value:function componentWillUnmount(){}},{key:'getToastStyle',value:function getToastStyle(){return{position:POSITION.ABSOLUTE,opacity:this.state.fadeAnim,width:'100%',elevation:9,paddingHorizontal:_reactNative.Platform.OS===_commonColor.PLATFORM.IOS?20:0,top:this.state.position===POSITION.TOP?30:undefined,bottom:this.state.position===POSITION.BOTTOM?this.getTop():undefined};}},{key:'getTop',value:function getTop(){if(_reactNative.Platform.OS===_commonColor.PLATFORM.IOS){if(this.state.isKeyboardVisible){return this.state.keyboardHeight;}return 30;}return 0;}},{key:'getButtonText',value:function getButtonText(buttonText){if(buttonText){if(buttonText.trim().length===0){return undefined;}return buttonText;}return undefined;}},{key:'getModalState',value:function getModalState(){return this.state.modalVisible;}},{key:'keyboardDidHide',value:function keyboardDidHide(){this.setState({keyboardHeight:0,isKeyboardVisible:false});}},{key:'keyboardDidShow',value:function keyboardDidShow(e){this.setState({keyboardHeight:e.endCoordinates.height,isKeyboardVisible:true});}},{key:'showToast',value:function showToast(_ref3){var config=_ref3.config;this.setState({modalVisible:true,text:config.text,buttonText:this.getButtonText(config.buttonText),type:config.type,position:config.position?config.position:POSITION.BOTTOM,supportedOrientations:config.supportedOrientations,style:config.style,buttonTextStyle:config.buttonTextStyle,buttonStyle:config.buttonStyle,textStyle:config.textStyle,onClose:config.onClose,swipeDisabled:config.swipeDisabled||false});if(this.closeTimeout){clearTimeout(this.closeTimeout);}if(config.duration!==0){var duration=config.duration>0?config.duration:1500;this.closeTimeout=setTimeout(this.closeToast.bind(this,'timeout'),duration);}_reactNative.Animated.timing(this.state.fadeAnim,{toValue:1,duration:200,useNativeDriver:false}).start();}},{key:'closeToast',value:function closeToast(reason){var _this2=this;clearTimeout(this.closeTimeout);_reactNative.Animated.timing(this.state.fadeAnim,{toValue:0,duration:200,useNativeDriver:false}).start(function(){_this2.closeModal(reason);_this2.state.pan.setValue({x:0,y:0});});}},{key:'render',value:function render(){var _this3=this;if(this.state.modalVisible){var _state$pan=this.state.pan,x=_state$pan.x,y=_state$pan.y;return _react2.default.createElement(_reactNative.Animated.View,_extends({},this.state.swipeDisabled?{}:this._panResponder.panHandlers,{style:[this.getToastStyle(),{transform:[{translateX:x},{translateY:y}]}],__source:{fileName:_jsxFileName,lineNumber:182}}),_react2.default.createElement(_Toast.Toast,{style:[this.state.style],danger:this.state.type==='danger',success:this.state.type==='success',warning:this.state.type==='warning',__source:{fileName:_jsxFileName,lineNumber:189}},_react2.default.createElement(_Text.Text,{style:this.state.textStyle,__source:{fileName:_jsxFileName,lineNumber:195}},this.state.text),this.state.buttonText&&_react2.default.createElement(_Button.Button,{style:this.state.buttonStyle,onPress:function onPress(){return _this3.closeToast('user');},__source:{fileName:_jsxFileName,lineNumber:197}},_react2.default.createElement(_Text.Text,{style:this.state.buttonTextStyle,__source:{fileName:_jsxFileName,lineNumber:201}},this.state.buttonText))));}return null;}}]);return ToastContainer;}(_react.Component);ToastContainer.propTypes=_extends({},_reactNative.ViewPropTypes);var StyledToastContainer=(0,_nativeBaseShoutemTheme.connectStyle)('NativeBase.ToastContainer',{},_mapPropsToStyleNames2.default)(ToastContainer);exports.ToastContainer=StyledToastContainer; + //# sourceMappingURL=ToastContainer.js.map +\ No newline at end of file +diff --git a/node_modules/native-base/src/basic/ToastContainer.js b/node_modules/native-base/src/basic/ToastContainer.js +index 4bd57ac..31c5586 100644 +--- a/node_modules/native-base/src/basic/ToastContainer.js ++++ b/node_modules/native-base/src/basic/ToastContainer.js +@@ -59,13 +59,13 @@ class ToastContainer extends Component { + } + + componentDidMount() { +- Keyboard.addListener('keyboardDidShow', this.keyboardDidShow); +- Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); ++ // Keyboard.addListener('keyboardDidShow', this.keyboardDidShow); ++ // Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); + } + + componentWillUnmount() { +- Keyboard.removeListener('keyboardDidShow', this.keyboardDidShow); +- Keyboard.removeListener('keyboardDidHide', this.keyboardDidHide); ++ // Keyboard.removeListener('keyboardDidShow', this.keyboardDidShow); ++ // Keyboard.removeListener('keyboardDidHide', this.keyboardDidHide); + } + + getToastStyle() { diff --git a/patches/patches.md b/patches/patches.md index a4f288edd4c..7eb5ebc50c1 100644 --- a/patches/patches.md +++ b/patches/patches.md @@ -1,5 +1,22 @@ This file describes the reason for the patches applied. +### native-base+2.15.2 +Created on **25/03/2024** + +#### Reason: +- Fixes a crash occurring when reopening the app while in background on Android (possibly even iOS but currently we don't have evidence) due to the usage of removed `keyboardDidShow` and `keyboardDidHide` events. +Remove this patch after removing `native-base`. + +### react-native-reanimated +Created on **18/03/2024** +This patch resolves [react-native-reanimated/issues/3286](https://github.com/software-mansion/react-native-reanimated/issues/3286) by applying [react-native-reanimated/pull/3298](https://github.com/software-mansion/react-native-reanimated/pull/3298). Remove this patch once bumped `react-native-reanimated` from `v2` to `v3` and checked the issue is gone. + +### react-native-pdf+6.4.0 +Created on **16/03/2024** + +#### Reason: +- Make PDF annotations on iOS read-only to align with Android behaviour. + ### react-native-vision-camera+2.15.4 Created on **24/07/2023** FIXME: remove this patch after this [PR](https://github.com/mrousavy/react-native-vision-camera/pull/1666) has been merged and a new vision camera version has been released. @@ -20,19 +37,7 @@ Created on **15/12/2021** - `getMacAddress` - `getMacAddressSync` -### react-native+0.69.9 -Created on **19/07/2023** - -#### Reason: -- This patch is going to add a missing POST_NOTIFICATIONS permission (Android 13) (remove this patch after updating to version 71 of React Native). - -### @types/react-native+0.69.9 -Created on **19/07/2023** - -#### Reason: -- Missing POST_NOTIFICATIONS permission type (remove this patch after updating to version 71 of React Native). - -### @types/react-native+0.69.6 +### @types/react-native+0.70.19 Created on **30/08/2022** #### Reason: @@ -96,7 +101,7 @@ Created on **16/08/2021** #### Reason: - implementation 'androidx.core:core:1.+' not compatible with the new gradle settings used by react-native 0.64.2 -### react-native+0.69.4 +### react-native+0.70.15 Created on **20/08/2021** #### Reason: @@ -120,7 +125,7 @@ Created on **16/09/2021** component, with this patch, doesn't use anymore the props permissionDialogTitle, permissionDialogMessage and buttonPositive. -### react-native+0.64.2 (Localizable.strings) +### react-native+0.70.15 (Localizable.strings) Created on **28/02/2022** @@ -150,14 +155,6 @@ Created on **29/08/2022** - This patch is going to fix a gradle issue that breaks the compile on android platform, due to gradle imcompatibility -### @react-navigation/material-top-tabs+5.3.1 - -Created on **01/12/2022** - -#### Reason: - -- This patch is going to add a missing prop to component definition, it can be removed once updating the library. - ### react-native-webview+11.26.1 Updated on **13/07/2023** diff --git a/patches/react-native+0.69.9.patch b/patches/react-native+0.70.15.patch similarity index 78% rename from patches/react-native+0.69.9.patch rename to patches/react-native+0.70.15.patch index 514b2d7f762..57b28621556 100644 --- a/patches/react-native+0.69.9.patch +++ b/patches/react-native+0.70.15.patch @@ -1,29 +1,9 @@ diff --git a/node_modules/react-native/Libraries/.DS_Store b/node_modules/react-native/Libraries/.DS_Store new file mode 100644 index 0000000..e69de29 -diff --git a/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js b/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js -index 227af12..2c59500 100644 ---- a/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js -+++ b/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js -@@ -68,6 +68,7 @@ const PERMISSIONS = Object.freeze({ - ANSWER_PHONE_CALLS: 'android.permission.ANSWER_PHONE_CALLS', - READ_PHONE_NUMBERS: 'android.permission.READ_PHONE_NUMBERS', - UWB_RANGING: 'android.permission.UWB_RANGING', -+ POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS' - }); - - /** -@@ -93,6 +94,7 @@ class PermissionsAndroid { - CALL_PHONE: string, - CAMERA: string, - GET_ACCOUNTS: string, -+ POST_NOTIFICATIONS: string, - PROCESS_OUTGOING_CALLS: string, - READ_CALENDAR: string, - READ_CALL_LOG: string, diff --git a/node_modules/react-native/React/AccessibilityResources/it.lproj/Localizable.strings b/node_modules/react-native/React/AccessibilityResources/it.lproj/Localizable.strings new file mode 100644 -index 0000000..00a9a38 +index 0000000..b0bc264 --- /dev/null +++ b/node_modules/react-native/React/AccessibilityResources/it.lproj/Localizable.strings @@ -0,0 +1,26 @@ @@ -53,8 +33,9 @@ index 0000000..00a9a38 +"expanded"="espanso"; +"collapsed"="compresso"; +"mixed"="misto"; +\ No newline at end of file diff --git a/node_modules/react-native/index.js b/node_modules/react-native/index.js -index d59ba34..8023167 100644 +index d59ba34..d0554fd 100644 --- a/node_modules/react-native/index.js +++ b/node_modules/react-native/index.js @@ -435,32 +435,16 @@ module.exports = { @@ -66,7 +47,7 @@ index d59ba34..8023167 100644 - 'ColorPropType has been removed from React Native. Migrate to ' + - "ColorPropType exported from 'deprecated-react-native-prop-types'.", - ); -+ return require("deprecated-react-native-prop-types").ColorPropType ++ return require("deprecated-react-native-prop-types").ColorPropType; }, get EdgeInsetsPropType(): $FlowFixMe { - invariant( @@ -74,7 +55,7 @@ index d59ba34..8023167 100644 - 'EdgeInsetsPropType has been removed from React Native. Migrate to ' + - "EdgeInsetsPropType exported from 'deprecated-react-native-prop-types'.", - ); -+ return require("deprecated-react-native-prop-types").EdgeInsetsPropType ++ return require("deprecated-react-native-prop-types").EdgeInsetsPropType; }, get PointPropType(): $FlowFixMe { - invariant( @@ -82,7 +63,7 @@ index d59ba34..8023167 100644 - 'PointPropType has been removed from React Native. Migrate to ' + - "PointPropType exported from 'deprecated-react-native-prop-types'.", - ); -+ return require("deprecated-react-native-prop-types").PointPropType ++ return require("deprecated-react-native-prop-types").PointPropType; }, get ViewPropTypes(): $FlowFixMe { - invariant( @@ -90,16 +71,16 @@ index d59ba34..8023167 100644 - 'ViewPropTypes has been removed from React Native. Migrate to ' + - "ViewPropTypes exported from 'deprecated-react-native-prop-types'.", - ); -+ return require("deprecated-react-native-prop-types").ViewPropTypes ++ return require("deprecated-react-native-prop-types").ViewPropTypes; }, }; diff --git a/node_modules/react-native/scripts/react-native-xcode.back.sh b/node_modules/react-native/scripts/react-native-xcode.back.sh new file mode 100755 -index 0000000..6f95a29 +index 0000000..927ec76 --- /dev/null +++ b/node_modules/react-native/scripts/react-native-xcode.back.sh -@@ -0,0 +1,187 @@ +@@ -0,0 +1,182 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# @@ -176,25 +157,19 @@ index 0000000..6f95a29 + ENTRY_FILE=${1:-index.js} +fi + -+if [[ $DEV != true && ! -f "$ENTRY_FILE" ]]; then -+ echo "error: Entry file $ENTRY_FILE does not exist. If you use another file as your entry point, pass ENTRY_FILE=myindex.js" >&2 -+ exit 2 -+fi -+ +# check and assign NODE_BINARY env +# shellcheck source=/dev/null +source "$REACT_NATIVE_DIR/scripts/node-binary.sh" + -+[ -z "$HERMES_CLI_PATH" ] && HERMES_CLI_PATH="$PODS_ROOT/hermes-engine/destroot/bin/hermesc" -+ -+if [[ -z "$USE_HERMES" && -f "$HERMES_CLI_PATH" ]]; then -+ echo "Enabling Hermes byte-code compilation. Disable with USE_HERMES=false if needed." -+ USE_HERMES=true -+fi ++HERMES_ENGINE_PATH="$PODS_ROOT/hermes-engine" ++[ -z "$HERMES_CLI_PATH" ] && HERMES_CLI_PATH="$HERMES_ENGINE_PATH/destroot/bin/hermesc" + -+if [[ $USE_HERMES == true && ! -f "$HERMES_CLI_PATH" ]]; then -+ echo "error: USE_HERMES is set to true but the hermesc binary could not be " \ -+ "found at ${HERMES_CLI_PATH}. Perhaps you need to run 'bundle exec pod install' or otherwise " \ ++# Hermes is enabled in new projects by default, so we cannot assume that USE_HERMES=1 is set as an envvar. ++# If hermes-engine is found in Pods, we can assume Hermes has not been disabled. ++# If hermesc is not available and USE_HERMES is either unset or true, show error. ++if [[ -f "$HERMES_ENGINE_PATH" && ! -f "$HERMES_CLI_PATH" ]]; then ++ echo "error: Hermes is enabled but the hermesc binary could not be found at ${HERMES_CLI_PATH}." \ ++ "Perhaps you need to run 'bundle exec pod install' or otherwise " \ + "point the HERMES_CLI_PATH variable to your custom location." >&2 + exit 2 +fi @@ -237,7 +212,7 @@ index 0000000..6f95a29 + +PACKAGER_SOURCEMAP_FILE= +if [[ $EMIT_SOURCEMAP == true ]]; then -+ if [[ $USE_HERMES == true ]]; then ++ if [[ $USE_HERMES != false ]]; then + PACKAGER_SOURCEMAP_FILE="$CONFIGURATION_BUILD_DIR/$(basename $SOURCEMAP_FILE)" + else + PACKAGER_SOURCEMAP_FILE="$SOURCEMAP_FILE" @@ -246,7 +221,7 @@ index 0000000..6f95a29 +fi + +# Hermes doesn't require JS minification. -+if [[ $USE_HERMES == true && $DEV == false ]]; then ++if [[ $USE_HERMES != false && $DEV == false ]]; then + EXTRA_ARGS="$EXTRA_ARGS --minify false" +fi + @@ -261,7 +236,7 @@ index 0000000..6f95a29 + $EXTRA_ARGS \ + $EXTRA_PACKAGER_ARGS + -+if [[ $USE_HERMES != true ]]; then ++if [[ $USE_HERMES == false ]]; then + cp "$BUNDLE_FILE" "$DEST/" + BUNDLE_FILE="$DEST/main.jsbundle" +else @@ -276,22 +251,23 @@ index 0000000..6f95a29 + fi + "$HERMES_CLI_PATH" -emit-binary $EXTRA_COMPILER_ARGS -out "$DEST/main.jsbundle" "$BUNDLE_FILE" + if [[ $EMIT_SOURCEMAP == true ]]; then -+ HBC_SOURCEMAP_FILE="$BUNDLE_FILE.map" ++ HBC_SOURCEMAP_FILE="$DEST/main.jsbundle.map" + "$NODE_BINARY" "$COMPOSE_SOURCEMAP_PATH" "$PACKAGER_SOURCEMAP_FILE" "$HBC_SOURCEMAP_FILE" -o "$SOURCEMAP_FILE" ++ rm "$HBC_SOURCEMAP_FILE" ++ rm "$PACKAGER_SOURCEMAP_FILE" + fi + BUNDLE_FILE="$DEST/main.jsbundle" +fi + +if [[ $DEV != true && ! -f "$BUNDLE_FILE" ]]; then -+ echo "error: File $BUNDLE_FILE does not exist. This must be a bug with" >&2 -+ echo "React Native, please report it here: https://github.com/facebook/react-native/issues" ++ echo "error: File $BUNDLE_FILE does not exist. This must be a bug with React Native, please report it here: https://github.com/facebook/react-native/issues" >&2 + exit 2 +fi diff --git a/node_modules/react-native/scripts/react-native-xcode.sh b/node_modules/react-native/scripts/react-native-xcode.sh -index 6f95a29..166c9d4 100755 +index 927ec76..477fc27 100755 --- a/node_modules/react-native/scripts/react-native-xcode.sh +++ b/node_modules/react-native/scripts/react-native-xcode.sh -@@ -155,6 +155,7 @@ fi +@@ -149,6 +149,7 @@ fi --dev $DEV \ --reset-cache \ --bundle-output "$BUNDLE_FILE" \ diff --git a/patches/react-native-mixpanel+1.2.0.patch b/patches/react-native-mixpanel+1.2.0.patch deleted file mode 100644 index 9ca04be8245..00000000000 --- a/patches/react-native-mixpanel+1.2.0.patch +++ /dev/null @@ -1,90 +0,0 @@ -diff --git a/node_modules/react-native-mixpanel/.DS_Store b/node_modules/react-native-mixpanel/.DS_Store -new file mode 100644 -index 0000000..e69de29 -diff --git a/node_modules/react-native-mixpanel/RNMixpanel/RNMixpanel.m b/node_modules/react-native-mixpanel/RNMixpanel/RNMixpanel.m -index 8564911..8a158db 100644 ---- a/node_modules/react-native-mixpanel/RNMixpanel/RNMixpanel.m -+++ b/node_modules/react-native-mixpanel/RNMixpanel/RNMixpanel.m -@@ -46,7 +46,12 @@ -(Mixpanel*) getInstance: (NSString *)name { - trackCrashes:trackCrashes - automaticPushTracking:automaticPushTracking - optOutTrackingByDefault:optOutTrackingByDefault]; -- -+ // disable A/B testing see https://pagopa.atlassian.net/browse/IA-42 -+ [instance setEnableVisualABTestAndCodeless: NO]; -+ instance.enableVisualABTestAndCodeless=NO; -+ // need to change mixpanel endpoint due to saving data in EU zone -+ // see https://www.pivotaltracker.com/story/show/171600487 -+ instance.serverURL = @"https://api-eu.mixpanel.com"; - // copy instances and add the new instance. then reassign instances - NSMutableDictionary *newInstances = [NSMutableDictionary dictionaryWithDictionary:instances]; - [newInstances setObject:instance forKey:apiToken]; -diff --git a/node_modules/react-native-mixpanel/android/.project b/node_modules/react-native-mixpanel/android/.project -new file mode 100644 -index 0000000..95512e0 ---- /dev/null -+++ b/node_modules/react-native-mixpanel/android/.project -@@ -0,0 +1,23 @@ -+ -+ -+ react-native-mixpanel -+ Project react-native-mixpanel created by Buildship. -+ -+ -+ -+ -+ org.eclipse.jdt.core.javabuilder -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectbuilder -+ -+ -+ -+ -+ -+ org.eclipse.jdt.core.javanature -+ org.eclipse.buildship.core.gradleprojectnature -+ -+ -diff --git a/node_modules/react-native-mixpanel/android/src/main/java/com/kevinejohn/RNMixpanel/RNMixpanelModule.java b/node_modules/react-native-mixpanel/android/src/main/java/com/kevinejohn/RNMixpanel/RNMixpanelModule.java -index 6239e11..a7352d4 100644 ---- a/node_modules/react-native-mixpanel/android/src/main/java/com/kevinejohn/RNMixpanel/RNMixpanelModule.java -+++ b/node_modules/react-native-mixpanel/android/src/main/java/com/kevinejohn/RNMixpanel/RNMixpanelModule.java -@@ -18,7 +18,7 @@ import org.json.JSONObject; - import java.util.Collections; - import java.util.HashMap; - import java.util.Map; -- -+import com.mixpanel.android.mpmetrics.MPConfig; - /** - * Mixpanel React Native module. - * Note that synchronized(instance) is used in methods because that's what MixpanelAPI.java recommends you do if you are keeping instances. -@@ -30,6 +30,13 @@ public class RNMixpanelModule extends ReactContextBaseJavaModule implements Life - public RNMixpanelModule(ReactApplicationContext reactContext) { - super(reactContext); - -+ -+ -+ // need to change mixpanel endpoint due to saving data in EU zone -+ // see https://www.pivotaltracker.com/story/show/171600487 -+ MPConfig.getInstance(reactContext).setEventsEndpoint("https://api-eu.mixpanel.com/track"); -+ MPConfig.getInstance(reactContext).setPeopleEndpoint("https://api-eu.mixpanel.com/engage"); -+ MPConfig.getInstance(reactContext).setGroupsEndpoint("https://api-eu.mixpanel.com/groups"); - // Get lifecycle notifications to flush mixpanel on pause or destroy - reactContext.addLifecycleEventListener(this); - } -diff --git a/node_modules/react-native-mixpanel/index.d.ts b/node_modules/react-native-mixpanel/index.d.ts -index c00ae8c..df23b29 100644 ---- a/node_modules/react-native-mixpanel/index.d.ts -+++ b/node_modules/react-native-mixpanel/index.d.ts -@@ -5,7 +5,7 @@ declare module 'react-native-mixpanel' { - initialize(): Promise - getDistinctId(): Promise - getSuperProperty(propertyName: string): Promise -- track(event: string, properties?: Object): Promise -+ track(event: string, properties?: {[key: string]: unknown}): Promise - flush(): Promise - disableIpAddressGeolocalization(): Promise - alias(alias: string, oldDistinctID?: string): Promise diff --git a/patches/react-native-pdf+6.4.0.patch b/patches/react-native-pdf+6.4.0.patch new file mode 100644 index 00000000000..c6c9fc0c29b --- /dev/null +++ b/patches/react-native-pdf+6.4.0.patch @@ -0,0 +1,27 @@ +diff --git a/node_modules/react-native-pdf/ios/RCTPdf/RCTPdfView.m b/node_modules/react-native-pdf/ios/RCTPdf/RCTPdfView.m +index 52aafd8..5efda0b 100644 +--- a/node_modules/react-native-pdf/ios/RCTPdf/RCTPdfView.m ++++ b/node_modules/react-native-pdf/ios/RCTPdf/RCTPdfView.m +@@ -188,12 +188,16 @@ const float MIN_SCALE = 1.0f; + } + + if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enableAnnotationRendering"])) { +- if (!_enableAnnotationRendering) { +- for (unsigned long i=0; i<_pdfView.document.pageCount; i++) { +- PDFPage *pdfPage = [_pdfView.document pageAtIndex:i]; +- for (unsigned long j=0; j _scheduler; + // Reanimated changes /start + if (isUIViewRegistry) { + NSMutableDictionary> *viewRegistry = [self valueForKey:@"_viewRegistry"]; ++ NSMutableDictionary> *> *toBeRemovedRegisterCopy = ++ [NSMutableDictionary dictionaryWithDictionary:_toBeRemovedRegister]; ++ for (NSNumber *key in _toBeRemovedRegister) { ++ toBeRemovedRegisterCopy[key] = [NSMutableSet setWithSet:_toBeRemovedRegister[key]]; ++ } + for (id toRemoveChild in _toBeRemovedRegister[containerTag]) { + NSInteger lastIndex = [container reactSubviews].count - 1; + if (lastIndex < 0) { +@@ -129,7 +134,7 @@ std::weak_ptr _scheduler; + ) { + // we don't want layout animations when removing modals or Screens of native-stack since it brings buggy + // behavior +- [_toBeRemovedRegister[container.reactTag] removeObject:toRemoveChild]; ++ [toBeRemovedRegisterCopy[container.reactTag] removeObject:toRemoveChild]; + [permanentlyRemovedChildren removeObject:toRemoveChild]; + + } else { +@@ -137,6 +142,7 @@ std::weak_ptr _scheduler; + viewRegistry[toRemoveChild.reactTag] = toRemoveChild; + } + } ++ _toBeRemovedRegister = toBeRemovedRegisterCopy; + + for (UIView *removedChild in permanentlyRemovedChildren) { + [self callAnimationForTree:removedChild parentTag:containerTag]; diff --git a/publiccode.yml b/publiccode.yml index 1ad58474092..0f5d665af71 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -5,11 +5,11 @@ publiccodeYmlVersion: '0.2' name: IO logo: "img/app-logo.svg" -releaseDate: '2024-02-02' +releaseDate: '2024-04-09' url: 'https://github.com/pagopa/io-app' applicationSuite: IO landingURL: 'https://io.italia.it/' -softwareVersion: 2.53.0-rc.0 +softwareVersion: 2.58.0-rc.0 developmentStatus: beta softwareType: standalone/mobile roadmap: 'https://io.italia.it/' diff --git a/react-native.config.js b/react-native.config.js index e93843b2f5d..6a69861eff1 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -12,6 +12,7 @@ module.exports = { }, assets: [ "./assets/fonts/TitilliumWeb", + "./assets/fonts/TitilliumSansPro", "./assets/fonts/ReadexPro", "./assets/fonts/RobotoMono", "./assets/fonts/DMMono" diff --git a/scripts/e2e_message/e2e_notifier.py b/scripts/e2e_message/e2e_notifier.py index 3a6d8ee776a..2b47bb77424 100644 --- a/scripts/e2e_message/e2e_notifier.py +++ b/scripts/e2e_message/e2e_notifier.py @@ -10,6 +10,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) SLACK_TOKEN = os.environ.get("IO_APP_SLACK_HELPER_BOT_TOKEN", None) +TEST_FILE = os.environ.get("TEST", None) tagged_people = [""] SLACK_CHANNEL = "#io_dev_app_feed" BUILD_ID = os.environ.get("BUILD_ID", None) @@ -27,8 +28,8 @@ def send_slack_message(): token=SLACK_TOKEN, ssl=ssl_context ) tags = " ".join(tagged_people) - message = "[E2E Tests] :warning: %s e2e tests have failed (<%s%s|here>)" % ( - tags, BASE_ACTION_URI, BUILD_ID) + message = "[E2E Tests] :warning: %s e2e test \`%s\` have failed (<%s%s|here>)" % ( + tags, TEST_FILE, BASE_ACTION_URI, BUILD_ID) message_blocks = [] message_blocks.append({ "type": "section", diff --git a/scripts/ts/common/jiraTicket/index.ts b/scripts/ts/common/jiraTicket/index.ts index ffc7e493e99..a464012539f 100644 --- a/scripts/ts/common/jiraTicket/index.ts +++ b/scripts/ts/common/jiraTicket/index.ts @@ -79,19 +79,16 @@ export const getJiraIdFromPrTitle = ( * Try to retrieve Jira tickets from pr title * and transforms them into {@link GenericTicket} * @param title + * @returns a promise of {@link JiraTicketRetrievalResults} containing the jira tickets or an error. If no tickets are found, an empty array is returned */ -export const getTicketsFromTitle = async ( +export const getTicketsFromTitle = ( title: string -): Promise => { - const maybeJiraId = await pipe( - getJiraIdFromPrTitle(title), - O.map(getJiraTickets), - O.toUndefined +): Promise => + pipe( + title, + getJiraIdFromPrTitle, + O.fold( + () => Promise.resolve([]), + jiraIds => getJiraTickets(jiraIds) + ) ); - - if (maybeJiraId) { - return maybeJiraId; - } else { - return [E.left(new Error("No Jira ticket found"))]; - } -}; diff --git a/scripts/ts/danger/updatePrTitle.tsx b/scripts/ts/danger/updatePrTitle.tsx index 4dc968dada8..7316df26d0c 100644 --- a/scripts/ts/danger/updatePrTitle.tsx +++ b/scripts/ts/danger/updatePrTitle.tsx @@ -43,7 +43,8 @@ const storyOrder = new Map([ const projectToScope = new Map([ ["IOAPPX", "Cross"], ["SFEQS", "Firma con IO"], - ["IODPAY", "IDPay"] + ["IODPAY", "IDPay"], + ["SIW", "IT Wallet"] ]); /** diff --git a/ts/App.tsx b/ts/App.tsx index 5adaf2441b0..ca90819e487 100644 --- a/ts/App.tsx +++ b/ts/App.tsx @@ -1,20 +1,20 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; +import { + IODSExperimentalContextProvider, + IOThemeContextProvider, + ToastProvider +} from "@pagopa/io-app-design-system"; import { StyleProvider } from "native-base"; import * as React from "react"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import { MenuProvider } from "react-native-popup-menu"; +import { SafeAreaProvider } from "react-native-safe-area-context"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { - IODSExperimentalContextProvider, - IOThemeContext, - IOThemes -} from "@pagopa/io-app-design-system"; +import RootContainer from "./RootContainer"; import { persistor, store } from "./boot/configureStoreAndPersistor"; import { LightModalProvider } from "./components/ui/LightModal"; -import RootContainer from "./RootContainer"; import theme from "./theme"; -import { ToastProvider } from "./components/Toast"; // Infer the `RootState` and `AppDispatch` types from the store itself export export type RootState = ReturnType; @@ -27,23 +27,25 @@ export type AppDispatch = typeof store.dispatch; export const App: React.FunctionComponent = () => ( - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + ); diff --git a/ts/__e2e__/login.e2e.ts b/ts/__e2e__/login.e2e.ts index 6a0b1371b26..b6f6e2b2ac0 100644 --- a/ts/__e2e__/login.e2e.ts +++ b/ts/__e2e__/login.e2e.ts @@ -1,9 +1,7 @@ import { loginWithSPID } from "./utils"; -describe("User Login using SPID", () => { - describe("when the user never logged in before", () => { - it("should let the user log in with SPID", async () => { - await loginWithSPID(); - }); +describe("User Login using SPID, when the user never logged in before", () => { + it("should let the user log in with SPID", async () => { + await loginWithSPID(); }); }); diff --git a/ts/__e2e__/payment.e2e.ts b/ts/__e2e__/payment.e2e.ts deleted file mode 100644 index 3e1c0ae9cf0..00000000000 --- a/ts/__e2e__/payment.e2e.ts +++ /dev/null @@ -1,166 +0,0 @@ -import I18n from "../i18n"; -import { formatNumberCentsToAmount } from "../utils/stringBuilder"; -import { e2eWaitRenderTimeout } from "./config"; -import { closeKeyboard, ensureLoggedIn } from "./utils"; - -describe("Payment", () => { - beforeEach(async () => { - await device.launchApp({ newInstance: true }); - await ensureLoggedIn(); - }); - - describe("When the user want to pay starting from a message", () => { - describe("And press back in the payment transaction summary screen", () => { - it("Should return to the message details screen", async () => { - await openPaymentFromMessage(); - const backButton = element(by.id("back-button-transaction-summary")); - await waitFor(backButton) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await backButton.tap(); - await waitFor(element(by.text(I18n.t("messageDetails.headerTitle")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); - - describe("when navigating to the wallet", () => { - it("then the wallet root screen should be visible", async () => { - await openPaymentFromMessage(); - - const backButton1 = element(by.id("back-button-transaction-summary")); - await waitFor(backButton1) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await backButton1.tap(); - - const backButton2 = element(by.id("back-button")); - await waitFor(backButton2) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await backButton2.tap(); - - const walletButton = element( - by.text(I18n.t("global.navigator.wallet")) - ); - await waitFor(walletButton) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await walletButton.tap(); - - await waitFor(element(by.text(I18n.t("wallet.payNotice")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); - }); - }); - - describe("And press cancel in the payment confirm screen", () => { - it("Should return to the message details screen", async () => { - await openPaymentFromMessage(); - await waitFor(element(by.text(I18n.t("wallet.continue")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await element(by.text(I18n.t("wallet.continue"))).tap(); - - await waitFor(element(by.text(I18n.t("wallet.ConfirmPayment.header")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const cancelButton = element(by.id("cancelPaymentButton")); - // const cancelButton = element(by.id("cancelPaymentButton")); - await waitFor(cancelButton).toExist().withTimeout(e2eWaitRenderTimeout); - await cancelButton.tap(); - // I18n.t("wallet.ConfirmPayment.confirmCancelPayment") - const confirmCancel = element( - by.label(I18n.t("wallet.ConfirmPayment.confirmCancelPayment")) - ).atIndex(0); - await waitFor(confirmCancel) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - await confirmCancel.tap(); - - await waitFor(element(by.text(I18n.t("messageDetails.headerTitle")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); - }); - - // TODO: this could be executed just one time until we have a way to reset the dev server between tests - it("Should allow the user to complete a payment", async () => { - await openPaymentFromMessage(); - await completePaymentFlow(); - }); - }); - - describe("When the user want to pay using the manual insertion", () => { - it("Should allow the user to complete a payment", async () => { - await element(by.text(I18n.t("global.navigator.wallet"))).tap(); - await element(by.text(I18n.t("wallet.payNotice"))).tap(); - - await element(by.id("barcodeScanBaseScreenTabInput")).tap(); - - const matchNoticeCodeInput = by.id("NoticeCodeInputMask"); - await waitFor(element(matchNoticeCodeInput)) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - - await element(matchNoticeCodeInput).typeText("123123123123123123"); - await element(by.id("EntityCodeInputMask")).typeText("12345678901"); - - // Close the keyboard - await closeKeyboard(); - - await element(by.text(I18n.t("global.buttons.continue"))).tap(); - - await completePaymentFlow(); - }); - }); -}); - -const completePaymentFlow = async () => { - await waitFor(element(by.text(I18n.t("wallet.continue")))) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - await element(by.text(I18n.t("wallet.continue"))).tap(); - - const matchConfirmPayment = by.text( - `${I18n.t("wallet.ConfirmPayment.pay")} ${formatNumberCentsToAmount( - 2322, - true - )}` - ); - await waitFor(element(matchConfirmPayment)) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - await element(matchConfirmPayment).tap(); - - await waitFor( - element( - by.text( - I18n.t("payment.paidConfirm", { - amount: formatNumberCentsToAmount(2322, true) - }) - ) - ) - ) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - - await element(by.text(I18n.t("wallet.outcomeMessage.cta.close"))).tap(); -}; - -const openPaymentFromMessage = async () => { - const messageWithPayment = element( - by.id(`MessageListItem_00000000000000000000000021`) - ); - await waitFor(messageWithPayment) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await messageWithPayment.tap(); - - const seeNoticeButton = element(by.text(I18n.t("messages.cta.seeNotice"))); - await waitFor(seeNoticeButton) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await seeNoticeButton.tap(); -}; diff --git a/ts/__e2e__/payment00.e2e.ts b/ts/__e2e__/payment00.e2e.ts new file mode 100644 index 00000000000..79748e53d91 --- /dev/null +++ b/ts/__e2e__/payment00.e2e.ts @@ -0,0 +1,20 @@ +import I18n from "../i18n"; +import { e2eWaitRenderTimeout } from "./config"; +import { ensureLoggedIn, openPaymentFromMessage } from "./utils"; + +describe("Payment", () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to pay starting from a message and press back in the payment transaction summary screen, it should return to the message details screen", async () => { + await openPaymentFromMessage(); + const backButton = element(by.id("back-button-transaction-summary")); + await waitFor(backButton).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await backButton.tap(); + await waitFor(element(by.text(I18n.t("messageDetails.headerTitle")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + }); +}); diff --git a/ts/__e2e__/payment01.e2e.ts b/ts/__e2e__/payment01.e2e.ts new file mode 100644 index 00000000000..0d73beff9a4 --- /dev/null +++ b/ts/__e2e__/payment01.e2e.ts @@ -0,0 +1,30 @@ +import I18n from "../i18n"; +import { e2eWaitRenderTimeout } from "./config"; +import { ensureLoggedIn, openPaymentFromMessage } from "./utils"; + +describe("Payment", () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to pay starting from a message and press back in the payment transaction summary screen, when navigating to the wallet then the wallet root screen should be visible", async () => { + await openPaymentFromMessage(); + + const backButton1 = element(by.id("back-button-transaction-summary")); + await waitFor(backButton1).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await backButton1.tap(); + + const backButton2 = element(by.id("back-button")); + await waitFor(backButton2).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await backButton2.tap(); + + const walletButton = element(by.text(I18n.t("global.navigator.wallet"))); + await waitFor(walletButton).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await walletButton.tap(); + + await waitFor(element(by.text(I18n.t("wallet.payNotice")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + }); +}); diff --git a/ts/__e2e__/payment02.e2e.ts b/ts/__e2e__/payment02.e2e.ts new file mode 100644 index 00000000000..c050a48077d --- /dev/null +++ b/ts/__e2e__/payment02.e2e.ts @@ -0,0 +1,37 @@ +import I18n from "../i18n"; +import { e2eWaitRenderTimeout } from "./config"; +import { ensureLoggedIn, openPaymentFromMessage } from "./utils"; + +describe("Payment", () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to pay starting from a message and press cancel in the payment confirm screen, it should return to the message details screen", async () => { + await openPaymentFromMessage(); + await waitFor(element(by.text(I18n.t("wallet.continue")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await element(by.text(I18n.t("wallet.continue"))).tap(); + + await waitFor(element(by.text(I18n.t("wallet.ConfirmPayment.header")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const cancelButton = element(by.id("cancelPaymentButton")); + // const cancelButton = element(by.id("cancelPaymentButton")); + await waitFor(cancelButton).toExist().withTimeout(e2eWaitRenderTimeout); + await cancelButton.tap(); + // I18n.t("wallet.ConfirmPayment.confirmCancelPayment") + const confirmCancel = element( + by.label(I18n.t("wallet.ConfirmPayment.confirmCancelPayment")) + ).atIndex(0); + await waitFor(confirmCancel).toExist().withTimeout(e2eWaitRenderTimeout); + await confirmCancel.tap(); + + await waitFor(element(by.text(I18n.t("messageDetails.headerTitle")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + }); +}); diff --git a/ts/__e2e__/payment03.e2e.ts b/ts/__e2e__/payment03.e2e.ts new file mode 100644 index 00000000000..d68ccd949d2 --- /dev/null +++ b/ts/__e2e__/payment03.e2e.ts @@ -0,0 +1,18 @@ +import { + completePaymentFlow, + ensureLoggedIn, + openPaymentFromMessage +} from "./utils"; + +describe("Payment", () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + // TODO: this could be executed just one time until we have a way to reset the dev server between tests + it("When the user want to pay starting from a message, it should allow the user to complete a payment", async () => { + await openPaymentFromMessage(); + await completePaymentFlow(); + }); +}); diff --git a/ts/__e2e__/payment04.e2e.ts b/ts/__e2e__/payment04.e2e.ts new file mode 100644 index 00000000000..eb75d994afe --- /dev/null +++ b/ts/__e2e__/payment04.e2e.ts @@ -0,0 +1,32 @@ +import I18n from "../i18n"; +import { e2eWaitRenderTimeout } from "./config"; +import { closeKeyboard, completePaymentFlow, ensureLoggedIn } from "./utils"; + +describe("Payment", () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to pay using the manual insertion, it should allow the user to complete a payment", async () => { + await element(by.text(I18n.t("global.navigator.wallet"))).tap(); + await element(by.text(I18n.t("wallet.payNotice"))).tap(); + + await element(by.id("barcodeScanBaseScreenTabInput")).tap(); + + const matchNoticeCodeInput = by.id("NoticeCodeInputMask"); + await waitFor(element(matchNoticeCodeInput)) + .toExist() + .withTimeout(e2eWaitRenderTimeout); + + await element(matchNoticeCodeInput).typeText("123123123123123123"); + await element(by.id("EntityCodeInputMask")).typeText("12345678901"); + + // Close the keyboard + await closeKeyboard(); + + await element(by.text(I18n.t("global.buttons.continue"))).tap(); + + await completePaymentFlow(); + }); +}); diff --git a/ts/__e2e__/utils.ts b/ts/__e2e__/utils.ts index ef0e68658fb..fe1194feeb1 100644 --- a/ts/__e2e__/utils.ts +++ b/ts/__e2e__/utils.ts @@ -1,3 +1,5 @@ +import { formatNumberCentsToAmount } from "../utils/stringBuilder"; +import I18n from "../i18n"; import { e2ePinChar1, e2ePinChar2, @@ -127,3 +129,51 @@ export const closeKeyboard = async () => { await element(by.label("Done")).atIndex(0).tap(); } }; + +export const openPaymentFromMessage = async () => { + const messageWithPayment = element( + by.id(`MessageListItem_00000000000000000000000019`) + ); + await waitFor(messageWithPayment) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await messageWithPayment.tap(); + + const seeNoticeButton = element(by.text(I18n.t("messages.cta.seeNotice"))); + await waitFor(seeNoticeButton) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await seeNoticeButton.tap(); +}; + +export const completePaymentFlow = async () => { + await waitFor(element(by.text(I18n.t("wallet.continue")))) + .toExist() + .withTimeout(e2eWaitRenderTimeout); + await element(by.text(I18n.t("wallet.continue"))).tap(); + + const matchConfirmPayment = by.text( + `${I18n.t("wallet.ConfirmPayment.pay")} ${formatNumberCentsToAmount( + 2322, + true + )}` + ); + await waitFor(element(matchConfirmPayment)) + .toExist() + .withTimeout(e2eWaitRenderTimeout); + await element(matchConfirmPayment).tap(); + + await waitFor( + element( + by.text( + I18n.t("payment.paidConfirm", { + amount: formatNumberCentsToAmount(2322, true) + }) + ) + ) + ) + .toExist() + .withTimeout(e2eWaitRenderTimeout); + + await element(by.text(I18n.t("wallet.outcomeMessage.cta.close"))).tap(); +}; diff --git a/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap b/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap index 04a4f1af920..88e04fba41a 100644 --- a/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap +++ b/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap @@ -79,6 +79,9 @@ Object { }, }, "paginatedById": Object {}, + "payments": Object { + "userSelectedPayments": Set {}, + }, "thirdPartyById": Object {}, }, "messagesStatus": Object {}, @@ -159,7 +162,9 @@ Object { "isDesignSystemEnabled": false, "isFingerprintEnabled": undefined, "isIdPayTestEnabled": false, + "isItWalletTestEnabled": false, "isMixpanelEnabled": null, + "isNewWalletSectionEnabled": false, "isPagoPATestEnabled": false, "isPnTestEnabled": false, "preferredCalendar": undefined, diff --git a/ts/boot/configurePushNotification.ts b/ts/boot/configurePushNotification.ts index 4530e6f9111..a501f9a8264 100644 --- a/ts/boot/configurePushNotification.ts +++ b/ts/boot/configurePushNotification.ts @@ -17,7 +17,6 @@ import { pageSize, remindersOptInEnabled } from "../config"; -import { setMixpanelPushNotificationToken } from "../mixpanel"; import { loadPreviousPageMessages, reloadAllMessages @@ -92,8 +91,6 @@ function configurePushNotifications() { PushNotification.configure({ // Called when token is generated onRegister: token => { - // send token to enable PN through Mixpanel - setMixpanelPushNotificationToken(token.token).then(constNull, constNull); // Dispatch an action to save the token in the store store.dispatch(updateNotificationsInstallationToken(token.token)); }, diff --git a/ts/boot/configureStoreAndPersistor.ts b/ts/boot/configureStoreAndPersistor.ts index 26b33faa001..ffa429cd003 100644 --- a/ts/boot/configureStoreAndPersistor.ts +++ b/ts/boot/configureStoreAndPersistor.ts @@ -1,6 +1,6 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import * as O from "fp-ts/lib/Option"; import _, { merge } from "lodash"; import { applyMiddleware, @@ -38,8 +38,8 @@ import { import { ContentState } from "../store/reducers/content"; import { entitiesPersistConfig } from "../store/reducers/entities"; import { - InstallationState, - INSTALLATION_INITIAL_STATE + INSTALLATION_INITIAL_STATE, + InstallationState } from "../store/reducers/installation"; import { NotificationsState } from "../store/reducers/notifications"; import { getInitialState as getInstallationInitialState } from "../store/reducers/notifications/installation"; @@ -53,7 +53,7 @@ import { configureReactotron } from "./configureRectotron"; /** * Redux persist will migrate the store to the current version */ -const CURRENT_REDUX_STORE_VERSION = 23; +const CURRENT_REDUX_STORE_VERSION = 26; // see redux-persist documentation: // https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md @@ -356,7 +356,47 @@ const migrations: MigrationManifest = { ..._.omit(persistedPreferences, "isExperimentalFeaturesEnabled") } }; - } + }, + // Version 24 + // Adds payments history archive persistence + "24": (state: PersistedState) => + merge(state, { + features: { + payments: { + history: { + archive: [] + } + } + } + }), + // Version 25 + // Adds new wallet section FF + "25": (state: PersistedState) => + merge(state, { + persistedPreferences: { + isNewWalletSectionEnabled: false + } + }), + // Version 26 + // Adds shouldShowPaymentsRedirectBanner persistence in feature wallet reducer + "26": (state: PersistedState) => + merge(state, { + features: { + wallet: { + preferences: { + shouldShowPaymentsRedirectBanner: true + } + } + } + }), + // Version 27 + // Adds it wallet section FF + "27": (state: PersistedState) => + merge(state, { + persistedPreferences: { + isItWalletTestEnabled: false + } + }) }; const isDebuggingInChrome = isDevEnv && !!window.navigator.userAgent; @@ -413,7 +453,10 @@ const sagaMiddleware = createSagaMiddleware( RTron ? { sagaMonitor: (RTron as any).createSagaMonitor() } : {} ); -function configureStoreAndPersistor(): { store: Store; persistor: Persistor } { +function configureStoreAndPersistor(): { + store: Store; + persistor: Persistor; +} { /** * If available use redux-devtool version of the compose function that allow * the inspection of the store from the devtool. diff --git a/ts/common/versionInfo/store/reducers/versionInfo.ts b/ts/common/versionInfo/store/reducers/versionInfo.ts index fec99d34b6a..99e280240f5 100644 --- a/ts/common/versionInfo/store/reducers/versionInfo.ts +++ b/ts/common/versionInfo/store/reducers/versionInfo.ts @@ -72,7 +72,10 @@ export const isAppSupportedSelector = createSelector( * Since the getAppVersion cannot change during the app execution, we can avoid forwarding it from the outside * @param state */ -export const isPagoPaSupportedSelector = createSelector( - [versionInfoDataSelector], - (versionInfo): boolean => isSupported(versionInfo?.min_app_version_pagopa) -); +export const isPagoPaSupportedSelector = (state: GlobalState) => + pipe( + state, + versionInfoDataSelector, + versionInfoStatusOrNull => versionInfoStatusOrNull?.min_app_version_pagopa, + isSupported + ); diff --git a/ts/components/CalendarList.tsx b/ts/components/CalendarList.tsx new file mode 100644 index 00000000000..b68a52748e6 --- /dev/null +++ b/ts/components/CalendarList.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { Calendar } from "react-native-calendar-events"; +import { + ContentWrapper, + RadioGroup, + RadioItem +} from "@pagopa/io-app-design-system"; +import { pipe } from "fp-ts/lib/function"; +import * as A from "fp-ts/lib/Array"; +import * as O from "fp-ts/lib/Option"; +import { convertLocalCalendarName } from "../utils/calendar"; +import { useIOSelector } from "../store/hooks"; +import { preferredCalendarSelector } from "../store/reducers/persistedPreferences"; + +type CalendarListProps = { + calendars: Array; + isLoading: boolean; + onCalendarSelected: (calendar: Calendar) => void; +}; + +const getCalendarsByAccount = ( + calendars: Array +): Array> => + pipe( + calendars, + A.filter(calendar => calendar.allowsModifications), + A.map(calendar => ({ + id: calendar.id, + value: convertLocalCalendarName(calendar.title), + description: calendar.source + })) + ); + +export const CalendarList = ({ + calendars = [], + isLoading = false, + onCalendarSelected +}: CalendarListProps) => { + const preferredCalendar = useIOSelector(preferredCalendarSelector); + + const handleSelectCalendar = (calendarId: string) => + pipe( + calendars, + A.findFirst(calendar => calendar.id === calendarId), + O.map(onCalendarSelected) + ); + + return ( + + + items={isLoading ? loadingCalendars : getCalendarsByAccount(calendars)} + onPress={handleSelectCalendar} + selectedItem={preferredCalendar?.id} + type="radioListItem" + /> + + ); +}; + +const loadingCalendars: Array> = A.makeBy(5, index => ({ + id: index.toString(), + disabled: true, + loadingProps: { skeletonDescription: true, state: true }, + value: "" +})); diff --git a/ts/components/DebugInfoOverlay.tsx b/ts/components/DebugInfoOverlay.tsx index aef80666ee3..6f558a9ac06 100644 --- a/ts/components/DebugInfoOverlay.tsx +++ b/ts/components/DebugInfoOverlay.tsx @@ -1,8 +1,3 @@ -import * as React from "react"; -import { StyleSheet, Pressable, SafeAreaView, View, Text } from "react-native"; -import { connect } from "react-redux"; -import { useState } from "react"; -import { widthPercentageToDP } from "react-native-responsive-screen"; import { HSpacer, IOColors, @@ -10,13 +5,18 @@ import { hexToRgba, makeFontStyleObject } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { useState } from "react"; +import { Pressable, SafeAreaView, StyleSheet, Text, View } from "react-native"; +import { widthPercentageToDP } from "react-native-responsive-screen"; +import { connect } from "react-redux"; import { ReduxProps } from "../store/actions/types"; +import { useIOSelector } from "../store/hooks"; import { currentRouteSelector } from "../store/reducers/navigation"; +import { isPagoPATestEnabledSelector } from "../store/reducers/persistedPreferences"; import { GlobalState } from "../store/reducers/types"; import { getAppVersion } from "../utils/appVersion"; import { clipboardSetStringWithFeedback } from "../utils/clipboard"; -import { useIOSelector } from "../store/hooks"; -import { isPagoPATestEnabledSelector } from "../store/reducers/persistedPreferences"; import PagoPATestIndicator from "./PagoPATestIndicator"; type Props = ReturnType & ReduxProps; @@ -27,6 +27,7 @@ const debugItemBorderColor = hexToRgba(IOColors.black, 0.1); const styles = StyleSheet.create({ versionContainer: { ...StyleSheet.absoluteFillObject, + top: -8, justifyContent: "flex-start", alignItems: "center", zIndex: 1000 diff --git a/ts/components/DebugPrettyPrint.tsx b/ts/components/DebugPrettyPrint.tsx index ad110fcff01..a9f11efec1b 100644 --- a/ts/components/DebugPrettyPrint.tsx +++ b/ts/components/DebugPrettyPrint.tsx @@ -1,3 +1,7 @@ +/* +WARNING: This component is not referenced anywhere, but is used +for development purposes. for development purposes. Don't REMOVE it! +*/ import { IOColors, Icon, diff --git a/ts/components/DevScreenButton.tsx b/ts/components/DevScreenButton.tsx deleted file mode 100644 index 31ffc1cd68d..00000000000 --- a/ts/components/DevScreenButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from "react"; -import { StyleSheet } from "react-native"; -import { Text as NBButtonText } from "native-base"; -import ButtonDefaultOpacity from "./ButtonDefaultOpacity"; - -type Props = Readonly<{ - onPress: () => void; -}>; - -const styles = StyleSheet.create({ - devButton: { - position: "absolute", - top: 20, - left: 3, - zIndex: 1000 - } -}); - -export const DevScreenButton: React.SFC = props => ( - - Dev - -); diff --git a/ts/components/EmailReadComponent.tsx b/ts/components/EmailReadComponent.tsx deleted file mode 100644 index fe59e2e44c8..00000000000 --- a/ts/components/EmailReadComponent.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { Platform, SafeAreaView, StyleSheet, View } from "react-native"; -import { Icon, VSpacer } from "@pagopa/io-app-design-system"; -import I18n from "../i18n"; -import { useIOSelector } from "../store/hooks"; -import { profileEmailSelector } from "../store/reducers/profile"; -import customVariables from "../theme/variables"; -import { ContextualHelpPropsMarkdown } from "./screens/BaseScreenComponent"; -import ScreenContent from "./screens/ScreenContent"; -import TopScreenComponent from "./screens/TopScreenComponent"; -import SectionStatusComponent from "./SectionStatus"; -import { BlockButtonsProps } from "./ui/BlockButtons"; -import FooterWithButtons from "./ui/FooterWithButtons"; -import { Body } from "./core/typography/Body"; -import { H3 } from "./core/typography/H3"; - -const styles = StyleSheet.create({ - flex: { - flex: 1 - }, - emailWithIcon: { - flexDirection: "row", - justifyContent: "flex-start", - alignItems: "center" - }, - content: { - paddingHorizontal: customVariables.contentPadding, - backgroundColor: customVariables.contentBackground, - flex: 1 - }, - icon: { - marginTop: Platform.OS === "android" ? 3 : 0, // correct icon position to align it with baseline of email text - marginRight: 8 - } -}); - -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "profile.data.email.contextualHelpTitle", - body: "profile.data.email.contextualHelpContent" -}; - -type Props = { - handleGoBack: () => void; - footerProps: BlockButtonsProps; -}; - -const EmailReadComponent = ({ handleGoBack, footerProps }: Props) => { - const optionEmail = useIOSelector(profileEmailSelector); - - return ( - - - - - {I18n.t("email.insert.label")} - - - - - - {O.isSome(optionEmail) &&

{optionEmail.value}

} -
- - {`${I18n.t("email.read.details")}`} -
-
- - -
-
- ); -}; - -export default EmailReadComponent; diff --git a/ts/components/FAQComponent.tsx b/ts/components/FAQComponent.tsx index 82ee4aab4c4..44056171933 100644 --- a/ts/components/FAQComponent.tsx +++ b/ts/components/FAQComponent.tsx @@ -2,12 +2,12 @@ import * as React from "react"; import { ComponentProps } from "react"; import { FAQType } from "../utils/faq"; import Accordion from "./ui/Accordion"; -import Markdown from "./ui/Markdown"; +import LegacyMarkdown from "./ui/Markdown/LegacyMarkdown"; type Props = Readonly<{ faqs: ReadonlyArray; - onLinkClicked?: ComponentProps["onLinkClicked"]; - shouldHandleLink?: ComponentProps["shouldHandleLink"]; + onLinkClicked?: ComponentProps["onLinkClicked"]; + shouldHandleLink?: ComponentProps["shouldHandleLink"]; }>; const FAQComponent: React.FunctionComponent = (props: Props) => ( diff --git a/ts/components/HorizontalScroll.tsx b/ts/components/HorizontalScroll.tsx deleted file mode 100644 index 6f2a86141b6..00000000000 --- a/ts/components/HorizontalScroll.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/** - * This component allows to display a carousel with rounded indicators at the bottom - */ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { - View, - Animated, - Dimensions, - Platform, - ScrollView, - StyleSheet, - NativeSyntheticEvent, - NativeScrollEvent -} from "react-native"; -import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import variables from "../theme/variables"; -import { roundToThirdDecimal } from "../utils/number"; -import { trackCarousel } from "../screens/authentication/analytics/carouselAnalytics"; - -type Props = { - cards: ReadonlyArray; - onCurrentElement?: (index: number) => void; - indexToScroll?: number; -}; - -const itemWidth = 10; // Radius of the indicators -const noWidth = 0; -const screenWidth = Dimensions.get("screen").width; - -const styles = StyleSheet.create({ - track: { - backgroundColor: IOColors.greyLight, - overflow: "hidden", - width: itemWidth, - height: itemWidth, - borderRadius: itemWidth / 2 - }, - - bar: { - backgroundColor: variables.brandPrimary, - borderRadius: itemWidth / 2, - width: itemWidth, - height: itemWidth - }, - - scrollView: { - flex: 1, - alignItems: "center", - justifyContent: "center" - }, - - barContainer: { - zIndex: itemWidth, - flexDirection: "row" - } -}); - -export const HorizontalScroll: React.FunctionComponent = ( - props: Props -) => { - const scrollOffset = - (props.indexToScroll ?? 0) * Dimensions.get("window").width; - const animVal = new Animated.Value(scrollOffset); - const scrollRef = React.useRef(null); - const { indexToScroll } = props; - - const [currentIndex, setCurrentIndex] = React.useState(0); - - React.useEffect(() => { - trackCarousel(currentIndex, props.cards); - }, [currentIndex, props.cards]); - - React.useEffect(() => { - pipe( - indexToScroll, - O.fromNullable, - O.map(_ => - setTimeout(() => { - if (scrollRef.current) { - scrollRef.current.scrollTo({ - x: scrollOffset, - y: 0, - animated: false - }); - } - }, 0) - ) - ); - }, [scrollRef, scrollOffset, indexToScroll]); - - const barArray = props.cards.map((_, i) => { - const scrollBarVal = animVal.interpolate({ - inputRange: [screenWidth * (i - 1), screenWidth * (i + 1)], - outputRange: [-itemWidth, itemWidth], - extrapolate: "clamp" - }); - - return ( - - - - ); - }); - - const handleScrollEvent = ( - event: NativeSyntheticEvent - ) => { - { - const currentIndex = Platform.select({ - ios: Math.round( - event.nativeEvent.contentOffset.x / Dimensions.get("window").width - ), - default: Math.round( - roundToThirdDecimal(event.nativeEvent.contentOffset.x) / - roundToThirdDecimal(Dimensions.get("window").width) - ) - }); - setCurrentIndex(currentIndex); - pipe( - props.onCurrentElement, - O.fromNullable, - O.map(onCurrElement => onCurrElement(currentIndex)) - ); - Animated.event([{ nativeEvent: { contentOffset: { x: animVal } } }])( - event - ); - } - }; - - return ( - - 1} - scrollEventThrottle={props.cards.length} - pagingEnabled={true} - onScroll={handleScrollEvent} - > - {props.cards} - - - {props.cards.length > 1 && ( - {barArray} - )} - - - ); -}; diff --git a/ts/components/IdpSuccessfulAuthentication.tsx b/ts/components/IdpSuccessfulAuthentication.tsx index 70867e243ad..bbfe97ff684 100644 --- a/ts/components/IdpSuccessfulAuthentication.tsx +++ b/ts/components/IdpSuccessfulAuthentication.tsx @@ -1,29 +1,37 @@ -/** - * A component to display a white tick on a blue background - */ -import * as React from "react"; -import { View, StatusBar, StyleSheet } from "react-native"; +import { useEffect } from "react"; +import { AccessibilityInfo } from "react-native"; +import { profileNameSelector } from "../store/reducers/profile"; +import I18n from "../i18n"; +import { useOnFirstRender } from "../utils/hooks/useOnFirstRender"; +import { trackIdpAuthenticationSuccessScreen } from "../screens/profile/analytics"; +import { useIOSelector } from "../store/hooks"; +import { loggedInIdpSelector } from "../store/reducers/authentication"; +import { OperationResultScreenContent } from "./screens/OperationResultScreenContent"; -import { IOIconSizeScale, Icon } from "@pagopa/io-app-design-system"; -import variables from "../theme/variables"; - -const styles = StyleSheet.create({ - container: { - backgroundColor: variables.brandPrimary, - flex: 1, - alignItems: "center", - justifyContent: "center" - } -}); - -const ICON_SIZE: IOIconSizeScale = 96; - -export const IdpSuccessfulAuthentication = () => ( - - - - -); +export const IdpSuccessfulAuthentication = () => { + const idp = useIOSelector(loggedInIdpSelector); + useOnFirstRender(() => { + trackIdpAuthenticationSuccessScreen(idp?.id); + }); + const name = useIOSelector(profileNameSelector); + // If the name is undefined, we set it to an empty string to avoid + // the pictogram shift up when the name is available. + const contentTitle = name + ? I18n.t("authentication.idp_login_success.contentTitle", { + name + }) + : " "; + // Announce the screen content when the name is available. + // Prefer an announce intead of setting the focus to the title + // because the screen is visible just for a short time. + useEffect(() => { + if (name) { + AccessibilityInfo.announceForAccessibility(contentTitle); + } + }, [contentTitle, name]); + return OperationResultScreenContent({ + pictogram: "success", + title: contentTitle, + testID: "idp-successful-authentication" + }); +}; diff --git a/ts/components/IdpsGrid.tsx b/ts/components/IdpsGrid.tsx index ee128423c19..2b8ce57c830 100644 --- a/ts/components/IdpsGrid.tsx +++ b/ts/components/IdpsGrid.tsx @@ -27,8 +27,12 @@ type OwnProps = { columnWrapperStyle?: StyleProp; contentContainerStyle?: StyleProp; headerComponentStyle?: StyleProp; - headerComponent?: React.ReactNode; - footerComponent?: React.ReactNode; + headerComponent?: React.ComponentProps< + typeof FlatList + >["ListHeaderComponent"]; + footerComponent?: React.ComponentProps< + typeof FlatList + >["ListFooterComponent"]; // Array of Identity Provider to show in the grid. idps: ReadonlyArray; // A callback function called when an Identity Provider is selected diff --git a/ts/components/IdpsGridRevamp.tsx b/ts/components/IdpsGridRevamp.tsx index 22ef9ce32ac..8107b88a3d2 100644 --- a/ts/components/IdpsGridRevamp.tsx +++ b/ts/components/IdpsGridRevamp.tsx @@ -25,9 +25,13 @@ import { LocalIdpsFallback } from "../utils/idps"; type OwnProps = { contentContainerStyle?: StyleProp; - footerComponent?: React.ReactNode; + footerComponent?: React.ComponentProps< + typeof FlatList + >["ListFooterComponent"]; headerComponentStyle?: StyleProp; - headerComponent?: React.ReactNode; + headerComponent?: React.ComponentProps< + typeof FlatList + >["ListHeaderComponent"]; // Array of Identity Provider to show in the grid. idps: ReadonlyArray; // A callback function called when an Identity Provider is selected diff --git a/ts/components/LandingCardComponent.tsx b/ts/components/LandingCardComponent.tsx index e652067bfa3..19f22956374 100644 --- a/ts/components/LandingCardComponent.tsx +++ b/ts/components/LandingCardComponent.tsx @@ -3,58 +3,69 @@ */ import * as React from "react"; -import { View, Dimensions, Image, ScrollView, StyleSheet } from "react-native"; -import { Col, Grid } from "react-native-easy-grid"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { Body } from "./core/typography/Body"; -import { H2 } from "./core/typography/H2"; +import { View, ScrollView, useWindowDimensions } from "react-native"; +import { + Body, + H3, + IOPictograms, + IOStyles, + Pictogram, + VSpacer +} from "@pagopa/io-app-design-system"; type Props = { id: number; - image: NodeRequire; + pictogramName: IOPictograms; title: string; content: string; accessibilityLabel?: string; accessibilityHint?: string; }; -const screenWidth = Dimensions.get("screen").width; +const VERTICAL_SPACING = 16; -const styles = StyleSheet.create({ - card: { - width: screenWidth, - alignItems: "center", - alignContent: "flex-start" - }, - image: { - width: screenWidth / 2, - height: screenWidth / 2, - resizeMode: "contain" - } -}); +export const LandingCardComponent = React.forwardRef( + (props, ref) => { + const screenDimension = useWindowDimensions(); + const screenWidth = screenDimension.width; + const { + accessibilityLabel, + accessibilityHint, + pictogramName, + title, + content + } = props; -export const LandingCardComponent: React.SFC = card => ( - - - - - - - -

- {card.title} -

- - {card.content} - - - -
-
-
+ return ( + + + + +

+ {title} +

+ + + {content} + + +
+
+ ); + } ); diff --git a/ts/components/LoadingSpinnerOverlay.tsx b/ts/components/LoadingSpinnerOverlay.tsx index 22271727f72..962fc81f4e0 100644 --- a/ts/components/LoadingSpinnerOverlay.tsx +++ b/ts/components/LoadingSpinnerOverlay.tsx @@ -6,9 +6,6 @@ import { hexToRgba } from "@pagopa/io-app-design-system"; import I18n from "../i18n"; -import { useIOSelector } from "../store/hooks"; -import { isDesignSystemEnabledSelector } from "../store/reducers/persistedPreferences"; -import ButtonDefaultOpacity from "./ButtonDefaultOpacity"; import { Overlay } from "./ui/Overlay"; import { IOStyles } from "./core/variables/IOStyles"; import { Body } from "./core/typography/Body"; @@ -37,50 +34,37 @@ const LoadingSpinnerOverlay = ({ loadingCaption, loadingOpacity = 0.7, onCancel -}: Props) => { - const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); - return ( - - - {loadingCaption || I18n.t("global.remoteStates.wait")} - +}: Props) => ( + + + {loadingCaption || I18n.t("global.remoteStates.wait")} + + + } + action={ + onCancel && ( + + - } - action={ - onCancel && ( - - {isDesignSystemEnabled ? ( - - ) : ( - - {I18n.t("global.buttons.cancel")} - - )} - - ) - } - /> - ) - } - > - {children} - - ); -}; + ) + } + /> + ) + } + > + {children} + +); export default LoadingSpinnerOverlay; diff --git a/ts/components/NavBarLabel.tsx b/ts/components/NavBarLabel.tsx deleted file mode 100644 index dc2f5c3be78..00000000000 --- a/ts/components/NavBarLabel.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { Platform, StyleSheet, Text } from "react-native"; -import { connect } from "react-redux"; -import { Locales, TranslationKeys } from "../../locales/locales"; -import I18n from "../i18n"; -import ROUTES from "../navigation/routes"; -import { messagesUnreadAndUnarchivedSelector } from "../features/messages/store/reducers/messagesStatus"; -import { preferredLanguageSelector } from "../store/reducers/persistedPreferences"; -import { GlobalState } from "../store/reducers/types"; -import { makeFontStyleObject } from "../theme/fonts"; -import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; - -type Routes = keyof typeof ROUTES | keyof typeof MESSAGES_ROUTES; - -type OwnProps = { - options: { - tintColor: string | null; - focused: boolean; - }; - routeName: string; -}; - -type Props = OwnProps & ReturnType; - -type RouteLabelMap = { [key in Routes]?: TranslationKeys }; -const ROUTE_LABEL: RouteLabelMap = { - MESSAGES_NAVIGATOR: "global.navigator.messages", - WALLET_HOME: "global.navigator.wallet", - SERVICES_NAVIGATOR: "global.navigator.services", - PROFILE_NAVIGATOR: "global.navigator.profile" -}; -const fallbackLabel = "unknown"; // fallback label - -const routeOrder = new Map([ - ["MESSAGES_NAVIGATOR", 1], - ["WALLET_HOME", 2], - ["SERVICES_NAVIGATOR", 3], - ["PROFILE_NAVIGATOR", 4] -]); - -const getLabel = (routeName: string, locale: Locales): string => - // "routeName as Routes" is assumed to be safe as explained @https://github.com/pagopa/io-app/pull/193#discussion_r192347234 - // adding fallback anyway -- better safe than sorry - pipe( - ROUTE_LABEL[routeName as Routes], - O.fromNullable, - O.fold( - () => fallbackLabel, - l => I18n.t(l, { locale }) - ) - ); -const styles = StyleSheet.create({ - labelStyle: { - ...makeFontStyleObject(Platform.select), - textAlign: "center" - } -}); - -const computeAccessibilityLabel = ( - section: string, - order: number, - unread?: number -) => - typeof unread === "undefined" - ? I18n.t("navigation.accessibility", { - section, - order - }) - : I18n.t("navigation.accessibilityWithBadge", { - section, - order, - unread - }); - -/** - * This Component is used to Render the Labels of the bottom navbar of the app - * translated in the preferred locale if it was selected - * @param props - */ -const NavBarLabel: React.FunctionComponent = (props: Props) => { - const { options, routeName, preferredLanguage, messagesUnread } = props; - const locale: Locales = pipe( - preferredLanguage, - O.fold( - () => I18n.locale, - l => l - ) - ); - const label = getLabel(routeName, locale); - const maybeOrder = O.fromNullable(routeOrder.get(routeName as Routes)); - const isSelected = options.focused - ? `${I18n.t("navigation.selected")}, ` - : ""; - - const unreadMessagesMap: Record = { - [MESSAGES_ROUTES.MESSAGES_NAVIGATOR]: messagesUnread.length - }; - - const computedUnreadMessages = unreadMessagesMap[routeName] || undefined; - - const panelAccessibilityLabel = computeAccessibilityLabel( - label, - O.getOrElse(() => 0)(maybeOrder), - computedUnreadMessages - ); - - return ( - - {label} - - ); -}; - -const mapStateToProps = (state: GlobalState) => ({ - preferredLanguage: preferredLanguageSelector(state), - messagesUnread: messagesUnreadAndUnarchivedSelector(state) -}); - -export default connect(mapStateToProps)(NavBarLabel); diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx deleted file mode 100644 index 504a705386b..00000000000 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ /dev/null @@ -1,279 +0,0 @@ -/** - * A component to remind the user to validate his email - */ -import { Millisecond } from "@pagopa/ts-commons/lib/units"; -import { pipe } from "fp-ts/lib/function"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { Content } from "native-base"; -import * as React from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { View, SafeAreaView } from "react-native"; -import { - LabelLink, - IOPictogramSizeScale, - Label, - Pictogram, - VSpacer -} from "@pagopa/io-app-design-system"; -import I18n from "../i18n"; - -import { - acknowledgeOnEmailValidation, - profileLoadRequest, - setEmailCheckAtStartupFailure, - startEmailValidation -} from "../store/actions/profile"; -import { - isProfileEmailValidatedSelector, - isProfileFirstOnBoardingSelector, - profileEmailSelector -} from "../store/reducers/profile"; -import { useIODispatch, useIOSelector } from "../store/hooks"; -import { emailValidationSelector } from "../store/reducers/emailValidation"; -import { emailAcknowledged } from "../store/actions/onboarding"; -import NavigationService from "../navigation/NavigationService"; -import ROUTES from "../navigation/routes"; -import { getFlowType } from "../utils/analytics"; -import { - trackEmailValidation, - trackEmailValidationSuccess, - trackEmailValidationSuccessConfirmed -} from "../screens/analytics/emailAnalytics"; -import { IOStyles } from "./core/variables/IOStyles"; -import FooterWithButtons from "./ui/FooterWithButtons"; -import { IOToast } from "./Toast"; -import { LightModalContextInterface } from "./ui/LightModal"; -import { withLightModalContext } from "./helpers/withLightModalContext"; -import BaseScreenComponent from "./screens/BaseScreenComponent"; - -const emailSentTimeout = 10000 as Millisecond; // 10 seconds -const profilePolling = 5000 as Millisecond; // 5 seconds - -const EMPTY_EMAIL = ""; -const VALIDATION_ILLUSTRATION_WIDTH: IOPictogramSizeScale = 80; - -type OwnProp = { - isOnboarding?: boolean; -}; - -type Props = LightModalContextInterface & OwnProp; - -const NewRemindEmailValidationOverlay = (props: Props) => { - const { isOnboarding, hideModal } = props; - const dispatch = useIODispatch(); - const optionEmail = useIOSelector(profileEmailSelector); - const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); - const emailValidation = useIOSelector(emailValidationSelector); - - const isFirstOnBoarding = useIOSelector(isProfileFirstOnBoardingSelector); - const flow = getFlowType(!!isOnboarding, isFirstOnBoarding); - - const [isValidateEmailButtonDisabled, setIsValidateEmailButtonDisabled] = - useState(false); - const timeout = useRef(); - const polling = useRef(); - - const email = pipe( - optionEmail, - O.getOrElse(() => EMPTY_EMAIL) - ); - - const sendEmailValidation = useCallback( - () => dispatch(startEmailValidation.request()), - [dispatch] - ); - - const acknowledgeEmail = useCallback( - () => dispatch(emailAcknowledged()), - [dispatch] - ); - - const reloadProfile = useCallback( - () => dispatch(profileLoadRequest()), - [dispatch] - ); - - const dispatchAcknowledgeOnEmailValidation = useCallback( - (maybeAcknowledged: O.Option) => - dispatch(acknowledgeOnEmailValidation(maybeAcknowledged)), - [dispatch] - ); - - // function to localize the title of the button. If the email is validated and if it is not, whether the confirmation email was sent or not - const buttonTitle = () => { - if (isEmailValidated) { - return I18n.t("global.buttons.continue"); - } else { - if (isValidateEmailButtonDisabled) { - return I18n.t("email.newvalidate.buttonlabelsent"); - } else { - return I18n.t("email.newvalidate.buttonlabelsentagain"); - } - } - }; - - // this function contol if the button is disabled. It is disabled if the email is sent and the timeout is active - const isButtonDisabled = () => { - if (isEmailValidated) { - return false; - } else { - return isValidateEmailButtonDisabled; - } - }; - - const handleSendEmailValidationButton = () => { - if (isEmailValidated) { - trackEmailValidationSuccessConfirmed(flow); - hideModal(); - if (isOnboarding) { - // if the user is in the onboarding flow and the email il correctly validated, - // the email validation flow is finished - acknowledgeEmail(); - } else { - if ( - O.isSome(emailValidation.emailCheckAtStartupFailed) && - emailValidation.emailCheckAtStartupFailed.value - ) { - acknowledgeEmail(); - dispatchAcknowledgeOnEmailValidation(O.none); - dispatch(setEmailCheckAtStartupFailure(O.none)); - } else { - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.PROFILE_DATA - }); - } - } - } else { - // send email validation only if it exists - pipe( - optionEmail, - O.map(_ => { - sendEmailValidation(); - }) - ); - } - }; - - const navigateToInsertEmail = () => { - dispatchAcknowledgeOnEmailValidation(O.none); - hideModal(); - }; - - const renderFooter = () => ( - - ); - - useEffect(() => { - // use polling to get the profile info, to check if the email is valid or not - // eslint-disable-next-line functional/immutable-data - polling.current = setInterval(() => reloadProfile(), profilePolling); - return () => { - hideModal(); - clearTimeout(timeout.current); - clearInterval(polling.current); - }; - }, [hideModal, reloadProfile]); - - useEffect(() => { - // send validation email KO - if (pot.isError(emailValidation.sendEmailValidationRequest)) { - IOToast.error(I18n.t("global.actions.retry")); - // send validation email OK - } else if (pot.isSome(emailValidation.sendEmailValidationRequest)) { - IOToast.show(I18n.t("email.newvalidate.toast")); - setIsValidateEmailButtonDisabled(true); - // eslint-disable-next-line functional/immutable-data - timeout.current = setTimeout(() => { - setIsValidateEmailButtonDisabled(false); - }, emailSentTimeout); - } - }, [emailValidation.sendEmailValidationRequest]); - - useEffect(() => { - if (isEmailValidated) { - clearInterval(polling.current); - trackEmailValidationSuccess(flow); - } else { - trackEmailValidation(flow); - } - }, [flow, isEmailValidated]); - - return ( - - - - - - - - - - - - - - - - - - {!isEmailValidated && ( - - - - {I18n.t("email.newvalidate.link")} - - - - )} - - {renderFooter()} - - - ); -}; -export default withLightModalContext(NewRemindEmailValidationOverlay); diff --git a/ts/components/Pinpad/KeyPad.tsx b/ts/components/Pinpad/KeyPad.tsx deleted file mode 100644 index db786793eab..00000000000 --- a/ts/components/Pinpad/KeyPad.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { ITuple2 } from "@pagopa/ts-commons/lib/tuples"; -import * as E from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; -import { Col, Grid, Row, Text as NBButtonText } from "native-base"; -import * as React from "react"; -import { Platform, StyleSheet, View } from "react-native"; -import { - IOColors, - IOIconSizeScale, - IOIcons, - Icon, - hexToRgba -} from "@pagopa/io-app-design-system"; -import { makeFontStyleObject } from "../../theme/fonts"; -import ButtonDefaultOpacity from "../ButtonDefaultOpacity"; - -// left -> the string to represent as text -// right -> the icon to represent with name and size -export type DigitRpr = E.Either< - string, - { name: IOIcons; size: IOIconSizeScale; accessibilityLabel: string } ->; -type Digit = ITuple2 void> | undefined; - -type Props = Readonly<{ - digits: ReadonlyArray>; - buttonType: "primary" | "light"; - isDisabled: boolean; -}>; - -// it generate buttons width of 56 -const radius = 18; -const BUTTON_DIAMETER = 56; -const opaqueButtonBackground = hexToRgba(IOColors.black, 0.1); - -const styles = StyleSheet.create({ - roundButton: { - paddingTop: 0, - paddingBottom: 0, - paddingRight: 0, - paddingLeft: 0, - marginBottom: 16, - alignSelf: "center", - justifyContent: "center", - width: BUTTON_DIAMETER, - height: BUTTON_DIAMETER, - borderRadius: BUTTON_DIAMETER / 2, - backgroundColor: opaqueButtonBackground - }, - transparent: { - backgroundColor: `transparent` - }, - buttonTextBase: { - ...makeFontStyleObject(Platform.select, "300"), - fontSize: 30, - lineHeight: 32, - marginBottom: -10 - }, - buttonTextLabel: { - fontSize: radius - 5 - }, - noPadded: { - paddingRight: 0 - } -}); - -const renderPinCol = ( - label: DigitRpr, - handler: () => void, - style: "digit" | "label", - key: string, - buttonType: "primary" | "light", - isDisabled: boolean -) => { - const buttonStyle = - style === "digit" - ? styles.roundButton - : style === "label" - ? [styles.roundButton, styles.transparent] - : undefined; - - const accessibilityLabel = pipe( - label, - E.fold( - () => undefined, - ic => ic.accessibilityLabel - ) - ); - - return ( - - - {pipe( - label, - E.fold( - l => ( - - {l} - - ), - ic => ( - - - - ) - ) - )} - - - ); -}; - -const renderPinRow = ( - digits: ReadonlyArray, - key: string, - buttonType: "primary" | "light", - isDisabled: boolean -) => ( - - {digits.map((el, i) => - el ? ( - renderPinCol( - el.e1, - el.e2, - E.isLeft(el.e1) ? "digit" : "label", - `pinpad-digit-${i}`, - buttonType, - isDisabled - ) - ) : ( - - ) - )} - -); - -/** - * Renders a virtual key pad. - * - * This component is used for typing PINs - */ -export class KeyPad extends React.PureComponent { - public render() { - return ( - - {this.props.digits.map((r, i) => - renderPinRow( - r, - `pinpad-row-${i}`, - this.props.buttonType, - this.props.isDisabled - ) - )} - - ); - } -} diff --git a/ts/components/Pinpad/index.tsx b/ts/components/Pinpad/index.tsx deleted file mode 100644 index 0051496c9cc..00000000000 --- a/ts/components/Pinpad/index.tsx +++ /dev/null @@ -1,385 +0,0 @@ -import { Tuple2 } from "@pagopa/ts-commons/lib/tuples"; -import { Millisecond } from "@pagopa/ts-commons/lib/units"; -import * as E from "fp-ts/lib/Either"; -import * as NAR from "fp-ts/lib/NonEmptyArray"; -import { debounce, shuffle } from "lodash"; -import * as React from "react"; -import { Alert, StyleSheet, View } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import I18n from "../../i18n"; -import { PinString } from "../../types/PinString"; -import { ComponentProps } from "../../types/react"; -import { BiometricsValidType } from "../../utils/biometrics"; -import { PIN_LENGTH, PIN_LENGTH_SIX } from "../../utils/constants"; -import { ShakeAnimation } from "../animations/ShakeAnimation"; -import { Body } from "../core/typography/Body"; -import { Link } from "../core/typography/Link"; -import { IOStyles } from "../core/variables/IOStyles"; -import { isDevEnv } from "../../utils/environment"; -import { defaultPin } from "../../config"; -import InputPlaceHolder from "./InputPlaceholder"; -import { DigitRpr, KeyPad } from "./KeyPad"; - -interface Props { - activeColor: string; - delayOnFailureMillis?: number; - clearOnInvalid?: boolean; - shufflePad?: boolean; - isFingerprintEnabled?: any; - isValidatingTask?: boolean; - biometryType?: any; - compareWithCode?: string; - inactiveColor: string; - disabled?: boolean; - buttonType: ComponentProps["buttonType"]; - onFulfill: (code: PinString, isValid: boolean) => void; - onCancel?: () => void; - onPinResetHandler?: () => void; - onFingerPrintReq?: () => void; - onDeleteLastDigit?: () => void; - remainingAttempts?: number; -} - -interface State { - value: string; - isDisabled: boolean; - pinLength: number; - pinPadValues: ReadonlyArray; -} - -const styles = StyleSheet.create({ - mediumText: { - fontSize: 18, - lineHeight: 21, - textAlign: "center" - } -}); - -const CANCEL_ICON_WIDTH = 24; -const ICON_WIDTH = 48; -const SHAKE_ANIMATION_DURATION = 600 as Millisecond; -const INPUT_MARGIN = 36; - -/** - * A customized CodeInput component. - */ -class Pinpad extends React.PureComponent { - private onFulfillTimeoutId?: number; - private onDelayOnFailureTimeoutId?: number; - private shakeAnimationRef = React.createRef(); - - /** - * Get the name of the icon (from icon font) to represent depending on - * the available biometry functionality available on the device - */ - private getBiometryIconName( - biometryPrintableSimpleType: BiometricsValidType - ): DigitRpr { - switch (biometryPrintableSimpleType) { - case "BIOMETRICS": - case "TOUCH_ID": - return E.right({ - name: "biomFingerprint", - size: ICON_WIDTH, - accessibilityLabel: I18n.t( - "identification.unlockCode.accessibility.fingerprint" - ) - }); - case "FACE_ID": - return E.right({ - name: "biomFaceID", - size: ICON_WIDTH, - accessibilityLabel: I18n.t( - "identification.unlockCode.accessibility.faceId" - ) - }); - } - } - - private deleteLastDigit = () => { - this.setState(prev => ({ - value: - prev.value.length > 0 - ? prev.value.slice(0, prev.value.length - 1) - : prev.value - })); - if (this.props.onDeleteLastDigit) { - this.props.onDeleteLastDigit(); - } - }; - - /** - * The pad can be composed by - * - strings - * - chars - * - icons (from icon font): they has to be declared as 'icon:' (width 48) or 'sicon:' (width 17) - */ - private pinPadDigits = (): ComponentProps["digits"] => { - const { pinPadValues } = this.state; - - return [ - [ - Tuple2(E.left(pinPadValues[1]), () => - this.handlePinDigit(pinPadValues[1]) - ), - Tuple2(E.left(pinPadValues[2]), () => - this.handlePinDigit(pinPadValues[2]) - ), - Tuple2(E.left(pinPadValues[3]), () => - this.handlePinDigit(pinPadValues[3]) - ) - ], - [ - Tuple2(E.left(pinPadValues[4]), () => - this.handlePinDigit(pinPadValues[4]) - ), - Tuple2(E.left(pinPadValues[5]), () => - this.handlePinDigit(pinPadValues[5]) - ), - Tuple2(E.left(pinPadValues[6]), () => - this.handlePinDigit(pinPadValues[6]) - ) - ], - [ - Tuple2(E.left(pinPadValues[7]), () => - this.handlePinDigit(pinPadValues[7]) - ), - Tuple2(E.left(pinPadValues[8]), () => - this.handlePinDigit(pinPadValues[8]) - ), - Tuple2(E.left(pinPadValues[9]), () => - this.handlePinDigit(pinPadValues[9]) - ) - ], - [ - this.props.isFingerprintEnabled && - this.props.biometryType && - this.props.onFingerPrintReq - ? Tuple2( - this.getBiometryIconName(this.props.biometryType), - this.props.onFingerPrintReq - ) - : undefined, - Tuple2(E.left(pinPadValues[0]), () => - this.handlePinDigit(pinPadValues[0]) - ), - Tuple2( - E.right({ - name: "cancel", - size: CANCEL_ICON_WIDTH, - accessibilityLabel: I18n.t( - "identification.unlockCode.accessibility.delete" - ) - }), - this.deleteLastDigit - ) - ] - ]; - }; - - private confirmResetAlert = () => - Alert.alert( - I18n.t("identification.forgetCode.confirmTitle"), - I18n.t( - this.props.isValidatingTask - ? "identification.forgetCode.confirmMsgWithTask" - : "identification.forgetCode.confirmMsg" - ), - [ - { - text: I18n.t("global.buttons.confirm"), - style: "default", - onPress: this.props.onPinResetHandler - }, - { - text: I18n.t("global.buttons.cancel"), - style: "cancel" - } - ], - { cancelable: false } - ); - - constructor(props: Props) { - super(props); - this.state = { - value: "", - isDisabled: false, - pinLength: PIN_LENGTH, - pinPadValues: NAR.range(0, 9).map(s => s.toString()) - }; - } - - public componentDidMount() { - const { pinPadValues } = this.state; - - const pinLength = - this.props.compareWithCode !== undefined - ? this.props.compareWithCode.length - : PIN_LENGTH_SIX; - - // we avoid to shuffle pin/code pad in dev env - const newPinPadValue = - this.props.shufflePad !== true ? pinPadValues : shuffle(pinPadValues); - - this.setState({ - pinLength, - pinPadValues: newPinPadValue - }); - } - - public componentWillUnmount() { - if (this.onFulfillTimeoutId) { - clearTimeout(this.onFulfillTimeoutId); - } else if (this.onDelayOnFailureTimeoutId) { - clearTimeout(this.onDelayOnFailureTimeoutId); - } - } - - private handleChangeText = (inputValue: string) => { - // if the component is disabled don't handle any input - if (this.props.disabled) { - return; - } - this.setState({ value: inputValue }); - - // Pin/code is fulfilled - if (inputValue.length === this.state.pinLength) { - const isValid = inputValue === this.props.compareWithCode; - - if (!isValid && this.props.clearOnInvalid) { - this.debounceClear(); - if (this.props.delayOnFailureMillis) { - // disable click keypad - this.setState({ - isDisabled: true - }); - - // re-enable after delayOnFailureMillis milliseconds - // eslint-disable-next-line - this.onDelayOnFailureTimeoutId = setTimeout(() => { - this.setState({ - isDisabled: false - }); - }, this.props.delayOnFailureMillis); - // start animation 'shake' - if (this.shakeAnimationRef.current) { - this.shakeAnimationRef.current.shake(); - } - } - } - - // Fire the callback asynchronously, otherwise this component - // will be unmounted before the render of the last bullet placeholder. - // eslint-disable-next-line - this.onFulfillTimeoutId = setTimeout(() => - this.props.onFulfill(inputValue as PinString, isValid) - ); - } - }; - - enterDefaultPin = () => { - if (!isDevEnv) { - return; - } - this.handleChangeText(defaultPin); - }; - - private handlePinDigit = (digit: string) => - this.handleChangeText( - `${this.state.value}${digit}`.substr(0, this.state.pinLength) - ); - - public debounceClear = debounce(() => { - this.setState({ value: "" }); - }, 100); - - public render() { - return ( - - - - {this.props.onPinResetHandler !== undefined && ( - - - {this.props.buttonType === "primary" ? ( - - {`${I18n.t("identification.unlockCode.reset.button")} `} - - {I18n.t("identification.unlockCode.reset.code")} - - - {I18n.t("global.symbols.question")} - - - ) : ( - - {`${I18n.t("identification.unlockCode.reset.button")} `} - - {I18n.t("identification.unlockCode.reset.code")} - - - {I18n.t("global.symbols.question")} - - - )} - - - - )} - - - - - - {isDevEnv && ( - - - this.enterDefaultPin()} - weight="Bold" - color="white" - > - {"Enter default pin (DevEnv Only)"} - - - )} - {this.props.onCancel && ( - - - - {I18n.t("global.buttons.cancel")} - - - )} - - ); - } -} - -export default Pinpad; diff --git a/ts/components/RemindEmailValidationOverlay.tsx b/ts/components/RemindEmailValidationOverlay.tsx index 1adf590058d..7ae72d791e7 100644 --- a/ts/components/RemindEmailValidationOverlay.tsx +++ b/ts/components/RemindEmailValidationOverlay.tsx @@ -58,7 +58,7 @@ import TouchableDefaultOpacity from "./TouchableDefaultOpacity"; import BlockButtons from "./ui/BlockButtons"; import FooterWithButtons from "./ui/FooterWithButtons"; import { LightModalContextInterface } from "./ui/LightModal"; -import Markdown from "./ui/Markdown"; +import LegacyMarkdown from "./ui/Markdown/LegacyMarkdown"; type OwnProp = { isOnboarding?: boolean; @@ -330,11 +330,13 @@ class RemindEmailValidationOverlay extends React.PureComponent { onPress: () => { if (this.props.isOnboarding) { NavigationService.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { isOnboarding: true } }); } else { NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.INSERT_EMAIL_SCREEN + screen: ROUTES.INSERT_EMAIL_SCREEN, + params: { isOnboarding: false } }); } }, @@ -391,14 +393,14 @@ class RemindEmailValidationOverlay extends React.PureComponent { {title} {!this.state.emailHasBeenValidate ? ( - {!this.props.isOnboarding ? I18n.t("email.validate.content2", { email }) : I18n.t("email.validate.content1", { email })} - + ) : ( - + = { - neutral: { - background: "turquoise-150", - stroke: "turquoise-850" - }, - error: { - background: "error-100", - stroke: "error-850" - }, - info: { - background: "info-100", - stroke: "info-850" - }, - success: { - background: "success-100", - stroke: "success-850" - }, - warning: { - background: "warning-100", - stroke: "warning-850" - } -}; - -type Props = Pick; - -const ToastNotification = ({ message, variant = "neutral", icon }: Props) => { - const colors = toastColorVariants[variant]; - - return ( - - - {message} - - {icon && } - - ); -}; - -const styles = StyleSheet.create({ - toast: { - borderRadius: IOAlertRadius, - borderWidth: 1, - padding: 16, - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between" - }, - content: { - paddingVertical: 2 - } -}); - -export { ToastNotification }; diff --git a/ts/components/Toast/ToastProvider.tsx b/ts/components/Toast/ToastProvider.tsx deleted file mode 100644 index 2c96f6bad63..00000000000 --- a/ts/components/Toast/ToastProvider.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { throttle } from "lodash"; -import React from "react"; -import { SafeAreaView, StyleSheet, View } from "react-native"; -import ReactNativeHapticFeedback from "react-native-haptic-feedback"; -import Animated, { - Easing, - SequencedTransition, - SlideInUp, - SlideOutUp -} from "react-native-reanimated"; -import { Dismissable } from "../ui/Dismissable"; -import { ToastNotification } from "./ToastNotification"; -import { Toast } from "./types"; -import { IOToastRef, useIOToast } from "./useIOToast"; -import { ToastContext } from "./context"; - -/** - * The maximum number of toasts that can be displayed at the same time - * If the number of the toasts exceeds this number, the oldest one will be removed - */ -export const MAX_TOAST_STACK_SIZE = 3; - -/** - * The time in milliseconds that a toast notification will be displayed - */ -export const TOAST_DURATION_TIME = 5000; - -/** - * This is the time in milliseconds between two toast notifications. - * This will throttle the toast notifications to avoid displaying too many of them at the same time - * and causing visual glitches. - */ -export const TOAST_THROTTLE_TIME = 500; - -type ToastNotificationStackItem = Toast & { id: number }; -type ToastNotificationStackItemProps = ToastNotificationStackItem & - Pick; - -/** - * A toast notification item that can be swiped to the right to dismiss it, with enter and exit animations - */ -const ToastNotificationStackItem = ({ - onDismiss, - ...toast -}: ToastNotificationStackItemProps) => ( - - - - - -); - -type ToastProviderProps = { - children: React.ReactNode; -}; - -export const ToastProvider = ({ children }: ToastProviderProps) => { - const toastId = React.useRef(1); - const [toasts, setToasts] = React.useState< - ReadonlyArray - >([]); - - const addToast = React.useCallback((toast: Toast): number => { - // eslint-disable-next-line functional/immutable-data - const id = toastId.current++; - setToasts(prevToasts => [{ id, ...toast }, ...prevToasts]); - - setTimeout(() => { - setToasts(prevToasts => prevToasts.filter(t => t.id !== id)); - }, TOAST_DURATION_TIME); - - if (toast.hapticFeedback) { - ReactNativeHapticFeedback.trigger(toast.hapticFeedback); - } - - return id; - }, []); - - const removeToast = React.useCallback((id: number) => { - setToasts(prevToasts => prevToasts.filter(t => t.id !== id)); - }, []); - - const removeToastAtIndex = (index: number) => { - setToasts(prevToasts => [ - ...prevToasts.slice(0, index), - ...prevToasts.slice(index + 1) - ]); - }; - - // If stack size exceed the maximum, remove the oldest toast - React.useEffect(() => { - if (toasts.length > MAX_TOAST_STACK_SIZE) { - removeToastAtIndex(MAX_TOAST_STACK_SIZE); - } - }, [toasts]); - - const removeAllToasts = React.useCallback(() => { - setToasts([]); - }, []); - - const contextValue = React.useMemo( - () => ({ - addToast: throttle(addToast, TOAST_THROTTLE_TIME), - removeToast: throttle(removeToast, TOAST_THROTTLE_TIME), - removeAllToasts - }), - [addToast, removeToast, removeAllToasts] - ); - - return ( - - - - - {toasts.map(toast => ( - removeToast(toast.id)} - /> - ))} - - - {children} - - ); -}; - -const InitializeToastRef = () => { - const toast = useIOToast(); - // eslint-disable-next-line functional/immutable-data - IOToastRef.current = toast; - return null; -}; - -const styles = StyleSheet.create({ - container: { - zIndex: 1000, - position: "absolute", - bottom: 0, - left: 0, - right: 0, - top: 0, - overflow: "visible" - }, - list: { - padding: 24 - } -}); diff --git a/ts/components/Toast/__tests__/ToastNotification.test.tsx b/ts/components/Toast/__tests__/ToastNotification.test.tsx deleted file mode 100644 index 1a081a6d50b..00000000000 --- a/ts/components/Toast/__tests__/ToastNotification.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { render } from "@testing-library/react-native"; -import React from "react"; -import { ToastNotification } from "../ToastNotification"; -import { Toast } from "../types"; - -describe("Test ToastNotification component", () => { - it.each([ - { message: "Hello", icon: "checkTick" }, - { message: "Hello", variant: "error" }, - { message: "Hello", variant: "info" }, - { message: "Hello", variant: "neutral" }, - { message: "Hello", variant: "success" }, - { message: "Hello", variant: "warning" } - ])("should match snapshot for props (%s)", toast => { - const component = render(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/ts/components/Toast/__tests__/__snapshots__/ToastNotification.test.tsx.snap b/ts/components/Toast/__tests__/__snapshots__/ToastNotification.test.tsx.snap deleted file mode 100644 index bcd5152bf21..00000000000 --- a/ts/components/Toast/__tests__/__snapshots__/ToastNotification.test.tsx.snap +++ /dev/null @@ -1,397 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', icon: 'checkTick' }) 1`] = ` - - - Hello - - - - - - - -`; - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', variant: 'error' }) 1`] = ` - - - Hello - - -`; - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', variant: 'info' }) 1`] = ` - - - Hello - - -`; - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', variant: 'neutral' }) 1`] = ` - - - Hello - - -`; - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', variant: 'success' }) 1`] = ` - - - Hello - - -`; - -exports[`Test ToastNotification component should match snapshot for props ({ message: 'Hello', variant: 'warning' }) 1`] = ` - - - Hello - - -`; diff --git a/ts/components/Toast/context.ts b/ts/components/Toast/context.ts deleted file mode 100644 index 66c24e437f8..00000000000 --- a/ts/components/Toast/context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext } from "react"; -import { Toast } from "./types"; - -export type ToastContext = { - addToast: (props: Toast) => number; - removeToast: (id: number) => void; - removeAllToasts: () => void; -}; - -export const ToastContext = createContext({ - addToast: () => 0, - removeToast: () => undefined, - removeAllToasts: () => undefined -}); diff --git a/ts/components/Toast/index.ts b/ts/components/Toast/index.ts deleted file mode 100644 index d1ce3b6d83a..00000000000 --- a/ts/components/Toast/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ToastNotification"; -export * from "./ToastProvider"; -export * from "./useIOToast"; diff --git a/ts/components/Toast/types.ts b/ts/components/Toast/types.ts deleted file mode 100644 index a66e0dd7be4..00000000000 --- a/ts/components/Toast/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HapticFeedbackTypes } from "react-native-haptic-feedback"; -import { IOIcons } from "@pagopa/io-app-design-system"; - -export type ToastVariant = "neutral" | "error" | "info" | "success" | "warning"; - -export type Toast = { - message: string; - variant?: ToastVariant; - icon?: IOIcons; - hapticFeedback?: keyof typeof HapticFeedbackTypes; -}; - -export type ToastOptions = Omit; diff --git a/ts/components/Toast/useIOToast.ts b/ts/components/Toast/useIOToast.ts deleted file mode 100644 index 47828d927ef..00000000000 --- a/ts/components/Toast/useIOToast.ts +++ /dev/null @@ -1,88 +0,0 @@ -import React, { MutableRefObject } from "react"; -import { ToastOptions } from "./types"; -import { ToastContext } from "./context"; - -export const useIOToast = () => { - const { addToast, removeToast, removeAllToasts } = - React.useContext(ToastContext); - - const show = React.useCallback( - (message: string, options?: ToastOptions) => { - addToast({ message, ...options }); - }, - [addToast] - ); - - const error = React.useCallback( - (message: string) => { - show(message, { - variant: "error", - icon: "errorFilled", - hapticFeedback: "notificationError" - }); - }, - [show] - ); - - const info = React.useCallback( - (message: string) => { - show(message, { - variant: "info", - icon: "infoFilled", - hapticFeedback: "impactMedium" - }); - }, - [show] - ); - - const success = React.useCallback( - (message: string) => { - show(message, { - variant: "success", - icon: "success", - hapticFeedback: "notificationSuccess" - }); - }, - [show] - ); - - const warning = React.useCallback( - (message: string) => { - show(message, { - variant: "warning", - icon: "warningFilled", - hapticFeedback: "notificationWarning" - }); - }, - [show] - ); - - return React.useMemo( - () => ({ - show, - error, - info, - success, - warning, - hide: removeToast, - hideAll: removeAllToasts - }), - [show, error, info, success, warning, removeToast, removeAllToasts] - ); -}; - -export type IOToast = ReturnType; - -export const IOToastRef = - React.createRef() as MutableRefObject; - -export const IOToast: IOToast = { - show: (message: string, options?: ToastOptions) => - IOToastRef.current?.show(message, options), - error: (message: string) => IOToastRef.current?.error(message), - warning: (message: string) => IOToastRef.current?.warning(message), - success: (message: string) => IOToastRef.current?.success(message), - info: (message: string) => IOToastRef.current?.info(message), - hideAll: () => IOToastRef.current?.hideAll(), - hide: (id: number) => IOToastRef.current?.hide(id) -}; diff --git a/ts/components/__tests__/NewRemindEmailValidationOverlay.test.tsx b/ts/components/__tests__/NewRemindEmailValidationOverlay.test.tsx deleted file mode 100644 index ae771f78bd3..00000000000 --- a/ts/components/__tests__/NewRemindEmailValidationOverlay.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import configureMockStore from "redux-mock-store"; -import { fireEvent } from "@testing-library/react-native"; -import I18n from "../../i18n"; - -import NewRemindEmailValidationOverlay from "../NewRemindEmailValidationOverlay"; -import { appReducer } from "../../store/reducers"; -import { applicationChangeState } from "../../store/actions/application"; -import { renderScreenWithNavigationStoreContext } from "../../utils/testWrapper"; -import { ServicesPreferencesModeEnum } from "../../../definitions/backend/ServicesPreferencesMode"; - -describe("NewRemindEmailValidationOverlay with isEmailValidated as true", () => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const mockStore = configureMockStore(); - - // eslint-disable-next-line functional/no-let - let finalState: ReturnType; - beforeAll(() => { - finalState = mockStore({ - ...globalState, - profile: pot.some({ - service_preferences_settings: { - mode: ServicesPreferencesModeEnum.AUTO - }, - is_email_validated: true, - email: "prova.prova@prova.com" - }) - }); - }); - - it("the components into the page should be render correctly", async () => { - const component = renderComponent(finalState); - expect(component).toBeDefined(); - expect(component.getByTestId("container-test")).not.toBeNull(); - expect(component.getByTestId("title-test")).toBeDefined(); - expect( - component.getByText(I18n.t("email.newvalidemail.title")) - ).toBeTruthy(); - expect( - component.getByText(I18n.t("email.newvalidemail.subtitle")) - ).toBeTruthy(); - const button = component.getByTestId("button-test"); - expect(button).toBeDefined(); - expect(component.getByText(I18n.t("global.buttons.continue"))).toBeTruthy(); - expect(button).not.toBeDisabled(); - if (button) { - fireEvent.press(button); - } - }); -}); - -describe("NewRemindEmailValidationOverlay with isEmailValidated as false", () => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const mockStore = configureMockStore(); - - // eslint-disable-next-line functional/no-let - let finalState: ReturnType; - - beforeAll(() => { - finalState = mockStore({ - ...globalState, - profile: pot.some({ - service_preferences_settings: { - mode: ServicesPreferencesModeEnum.AUTO - }, - is_email_validated: false, - email: "prova.prova@prova.com" - }) - }); - }); - - it("the components into the page should be render correctly", async () => { - const component = renderComponent(finalState); - expect(component).toBeDefined(); - expect(component.getByTestId("container-test")).not.toBeNull(); - expect(component.getByTestId("title-test")).toBeDefined(); - expect(component.getByText(I18n.t("email.newvalidate.title"))).toBeTruthy(); - expect( - component.getByText(I18n.t("email.newvalidate.subtitle")) - ).toBeTruthy(); - expect(component.getByTestId("link-test")).toBeDefined(); - const button = component.getByTestId("button-test"); - expect(button).toBeDefined(); - expect( - component.getByText(I18n.t("email.newvalidate.buttonlabelsentagain")) - ).toBeTruthy(); - expect(button).not.toBeDisabled(); - if (button) { - fireEvent.press(button); - } - - setTimeout(() => { - expect( - component.getByText(I18n.t("email.newvalidate.buttonlabelsentagain")) - ).not.toBeDisabled(); - }, 10000); - }); -}); - -const renderComponent = (globalStateProp?: any) => - renderScreenWithNavigationStoreContext( - NewRemindEmailValidationOverlay, - "DUMMY", - {}, - globalStateProp - ); diff --git a/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap b/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap index 5f42ae771f3..3c8c2f69eb6 100644 --- a/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap +++ b/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap @@ -140,6 +140,12 @@ exports[`LoadingSpinnerOverlay Should match all-properties and loading snapshot /> @@ -303,8 +332,11 @@ exports[`LoadingSpinnerOverlay Should match all-properties and loading snapshot } > diff --git a/ts/components/animations/ShakeAnimation.tsx b/ts/components/animations/ShakeAnimation.tsx deleted file mode 100644 index 0c7383b50b8..00000000000 --- a/ts/components/animations/ShakeAnimation.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from "react"; -import { Animated, Easing } from "react-native"; - -type Props = Readonly<{ - duration: number; -}>; -/** - * - * This component create a custom animation called 'shake' - */ -export class ShakeAnimation extends React.PureComponent { - private animatedValue: Animated.Value; - - constructor(props: Props) { - super(props); - this.animatedValue = new Animated.Value(0); - } - - public shake = () => { - this.animatedValue.setValue(0); - // start animation - Animated.timing(this.animatedValue, { - toValue: 1, - duration: this.props.duration, - useNativeDriver: true, - easing: Easing.ease - }).start(); - }; - - public render() { - // animation interpolate from left to right and the other way around - const shaker = this.animatedValue.interpolate({ - inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1], - outputRange: [0, -10, 10, -10, 10, -10, 0] - }); - return ( - - {this.props.children} - - ); - } -} diff --git a/ts/components/bottomSheet/AccessibilityContent.tsx b/ts/components/bottomSheet/AccessibilityContent.tsx deleted file mode 100644 index 32dc84020e6..00000000000 --- a/ts/components/bottomSheet/AccessibilityContent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; -import { View, Modal } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../core/variables/IOStyles"; - -type Props = { - header: React.ReactNode; - content: React.ReactNode; -}; -/** - * Accessibility version of a BottomSheet including a header and it's content - * @param header - * @param content - */ -export const AccessibilityContent = ({ header, content }: Props) => ( - - - {header} - {content} - -); diff --git a/ts/components/bottomSheet/BlurredBackgroundComponent.tsx b/ts/components/bottomSheet/BlurredBackgroundComponent.tsx deleted file mode 100644 index 67118c7fffe..00000000000 --- a/ts/components/bottomSheet/BlurredBackgroundComponent.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; -import { StyleSheet } from "react-native"; -import { IOColors, hexToRgba } from "@pagopa/io-app-design-system"; -import TouchableDefaultOpacity from "../TouchableDefaultOpacity"; - -const opaqueBgColor = hexToRgba(IOColors.black, 0.2); - -/** - * Simple component used as background when the bottom sheet pops up to blur the background and demands its closing when tapping on it - * @param onPress - */ -export const BlurredBackgroundComponent = (onPress: () => void) => ( - -); diff --git a/ts/components/cie/CieNotSupported.tsx b/ts/components/cie/CieNotSupported.tsx index f354fc433b1..1073298d317 100644 --- a/ts/components/cie/CieNotSupported.tsx +++ b/ts/components/cie/CieNotSupported.tsx @@ -1,80 +1,96 @@ -import { List, ListItem } from "native-base"; -import * as React from "react"; -import { useState } from "react"; +/* eslint-disable no-console */ +import React, { useRef } from "react"; import { Platform, View } from "react-native"; import { - IOColors, - IOIcons, - Icon, - HSpacer, + Alert, + Body, + ContentWrapper, + FeatureInfo, + GradientScrollView, + H3, + IOStyles, + Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { useFocusEffect, useNavigation } from "@react-navigation/native"; import I18n from "../../i18n"; -import Markdown from "../ui/Markdown"; -import { Body } from "../core/typography/Body"; +import { useIOSelector } from "../../store/hooks"; +import { + hasApiLevelSupportSelector, + hasNFCFeatureSelector +} from "../../store/reducers/cie"; +import { setAccessibilityFocus } from "../../utils/accessibility"; -type Props = { - hasCieApiLevelSupport: boolean; - hasCieNFCFeature: boolean; -}; +const CieNotSupported = () => { + const hasApiLevelSupport = useIOSelector(hasApiLevelSupportSelector); + const hasCieApiLevelSupport = pot.getOrElse(hasApiLevelSupport, false); + const hasNFCFeature = useIOSelector(hasNFCFeatureSelector); + const hasCieNFCFeature = pot.getOrElse(hasNFCFeature, false); + const accessibilityFirstFocuseViewRef = useRef(null); + const navigation = useNavigation(); -const ICON_SIZE = 24; -const okColor: IOColors = "green"; -const koColor: IOColors = "red"; -const okIcon: IOIcons = "ok"; -const koIcon: IOIcons = "errorFilled"; -const markDownElements = 2; -const CieNotSupported: React.FunctionComponent = props => { - const [markdownLoaded, setMarkdownLoaded] = useState(0); - const handleMarkdownLoaded = () => setMarkdownLoaded(s => s + 1); - return ( - - - {I18n.t("authentication.landing.cie_unsupported.body")} - + useFocusEffect(() => setAccessibilityFocus(accessibilityFirstFocuseViewRef)); - {Platform.OS === "android" && ( - - - - {I18n.t("authentication.landing.cie_unsupported.android_desc")} - - - {markdownLoaded === markDownElements && ( - - - - - - {I18n.t( - "authentication.landing.cie_unsupported.os_version_unsupported" - )} - - - - - - - - - {I18n.t( - "authentication.landing.cie_unsupported.nfc_incompatible" - )} - - - - )} - - )} - + return ( + navigation.goBack(), + testID: "close-button" + }} + > + + + + + + +

+ {I18n.t("authentication.landing.cie_unsupported.title")} +

+
+ + + {I18n.t("authentication.landing.cie_unsupported.body")} + + + + + + + {Platform.OS === "android" && !hasCieNFCFeature ? ( + + ) : ( + Platform.OS === "android" && + hasCieApiLevelSupport && ( + + ) + )} +
+
); }; diff --git a/ts/components/cie/CieRequestAuthenticationOverlay.tsx b/ts/components/cie/CieRequestAuthenticationOverlay.tsx index c451b430cfb..fdf8022daee 100644 --- a/ts/components/cie/CieRequestAuthenticationOverlay.tsx +++ b/ts/components/cie/CieRequestAuthenticationOverlay.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { createRef, useEffect } from "react"; -import { View, Platform, SafeAreaView, StyleSheet } from "react-native"; +import { View, Platform, StyleSheet } from "react-native"; import WebView from "react-native-webview"; import { WebViewErrorEvent, @@ -22,7 +22,6 @@ import { getIdpLoginUri } from "../../utils/login"; import { closeInjectedScript } from "../../utils/webview"; import { IOStyles } from "../core/variables/IOStyles"; import { withLoadingSpinner } from "../helpers/withLoadingSpinner"; -import GenericErrorComponent from "../screens/GenericErrorComponent"; import { lollipopKeyTagSelector } from "../../features/lollipop/store/reducers/lollipop"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { isMixpanelEnabled } from "../../store/reducers/persistedPreferences"; @@ -32,6 +31,7 @@ import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selec import { isCieLoginUatEnabledSelector } from "../../features/cieLogin/store/selectors"; import { cieFlowForDevServerEnabled } from "../../features/cieLogin/utils"; import { selectedIdentityProviderSelector } from "../../store/reducers/authentication"; +import { OperationResultScreenContent } from "../screens/OperationResultScreenContent"; const styles = StyleSheet.create({ errorContainer: { @@ -307,15 +307,22 @@ const CieWebView = (props: Props) => { const ErrorComponent = ( props: { onRetry: () => void } & Pick ) => ( - - + - +
); /** diff --git a/ts/components/core/accordion/IOAccordion.tsx b/ts/components/core/accordion/IOAccordion.tsx deleted file mode 100644 index 36332200936..00000000000 --- a/ts/components/core/accordion/IOAccordion.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { StyleSheet } from "react-native"; -import themeVariables from "../../../theme/variables"; -import { H3 } from "../typography/H3"; -import { IOStyles } from "../variables/IOStyles"; -import { RawAccordion } from "./RawAccordion"; - -type Props = Omit, "header"> & { - title: string; -}; - -const styles = StyleSheet.create({ - header: { - marginVertical: themeVariables.contentPadding - } -}); - -/** - * A simplified accordion that accepts a title and one child and uses {@link RawAccordion} - * @param props - * @constructor - */ -export const IOAccordion = (props: Props): React.ReactElement => ( - - {props.title} - - } - > - {props.children} - -); diff --git a/ts/components/core/accordion/__test__/RawAccordion.test.tsx b/ts/components/core/accordion/__test__/RawAccordion.test.tsx index c8d1626b732..38b37c94750 100644 --- a/ts/components/core/accordion/__test__/RawAccordion.test.tsx +++ b/ts/components/core/accordion/__test__/RawAccordion.test.tsx @@ -1,8 +1,7 @@ +import { IOColors } from "@pagopa/io-app-design-system"; import { fireEvent, render } from "@testing-library/react-native"; -import { View } from "react-native"; import * as React from "react"; -import { IOColors } from "@pagopa/io-app-design-system"; -import Fingerprint from "../../../../../img/test/fingerprint.svg"; +import { View } from "react-native"; import I18n from "../../../../i18n"; import { Body } from "../../typography/Body"; import { H3 } from "../../typography/H3"; @@ -121,7 +120,6 @@ const renderRawAccordion = ( }} header={ -

{headerText}

} diff --git a/ts/components/core/fonts.ts b/ts/components/core/fonts.ts index bf0fb1be368..56f141b2555 100644 --- a/ts/components/core/fonts.ts +++ b/ts/components/core/fonts.ts @@ -24,6 +24,10 @@ const fonts = { android: "TitilliumWeb", ios: "Titillium Web" }), + TitilliumSansPro: Platform.select({ + android: "TitilliumSansPro", + ios: "Titillium Sans Pro" + }), ReadexPro: Platform.select({ android: "ReadexPro", ios: "Readex Pro" diff --git a/ts/components/core/selection/__test__/__snapshots__/RemoteSwitch.test.tsx.snap b/ts/components/core/selection/__test__/__snapshots__/RemoteSwitch.test.tsx.snap index 56d389d3df0..673a56fba5e 100644 --- a/ts/components/core/selection/__test__/__snapshots__/RemoteSwitch.test.tsx.snap +++ b/ts/components/core/selection/__test__/__snapshots__/RemoteSwitch.test.tsx.snap @@ -52,7 +52,6 @@ exports[`RemoteSwitch tests Snapshot for pot.noneError 1`] = ` align="xMidYMid" bbHeight={24} bbWidth={24} - color={4278219750} focusable={false} height={24} importantForAccessibility="no-hide-descendants" @@ -75,19 +74,26 @@ exports[`RemoteSwitch tests Snapshot for pot.noneError 1`] = ` }, ] } - tintColor={4278219750} + tintColor="#0073E6" vbHeight={24} vbWidth={24} width={24} > - + ; - -type OwnProps = ExternalTypographyProps< - TypographyProps ->; - -/* Common typographic styles */ -export const ctaFontSize = 16; -export const ctaLineHeight = 20; -export const ctaDefaultColor: AllowedColors = IOThemeLight["textBody-default"]; -/* New typographic styles */ -const ctaFontName: IOFontFamily = "ReadexPro"; -const ctaDefaultWeight: AllowedWeight = "Regular"; - -/** - * Typography component to render `H4` text with font size {@link fontSize} and fontFamily {@link fontName}. - * default values(if not defined) are weight: `Bold`, color: `bluegreyDark` - * @param props - * @constructor - */ -export const CTA: React.FunctionComponent = props => - useTypographyFactory({ - ...props, - defaultWeight: ctaDefaultWeight, - defaultColor: ctaDefaultColor, - font: ctaFontName, - fontStyle: { fontSize: ctaFontSize, lineHeight: ctaLineHeight } - }); diff --git a/ts/components/core/typography/Label.tsx b/ts/components/core/typography/Label.tsx index bed1db91c37..92ac7e207c7 100644 --- a/ts/components/core/typography/Label.tsx +++ b/ts/components/core/typography/Label.tsx @@ -1,8 +1,8 @@ import { FontSize, fontSizeMapping, - type IOColors, - type IOColorsStatusForeground + IOColors, + IOColorsStatusForeground } from "@pagopa/io-app-design-system"; import { IOFontFamily, IOFontWeight } from "../fonts"; import { ExternalTypographyProps, TypographyProps } from "./common"; diff --git a/ts/components/core/variables/IOStyleVariables.ts b/ts/components/core/variables/IOStyleVariables.ts index 88a20791963..a32f81e033a 100644 --- a/ts/components/core/variables/IOStyleVariables.ts +++ b/ts/components/core/variables/IOStyleVariables.ts @@ -1,6 +1,9 @@ +import { IOSpacer } from "@pagopa/io-app-design-system"; + /** * A collection of default style variables used within IO App. */ export const IOStyleVariables = { + defaultSpaceBetweenPictogramAndText: 24 as IOSpacer, switchWidth: 51 }; diff --git a/ts/components/countdown/CountdownProvider.tsx b/ts/components/countdown/CountdownProvider.tsx new file mode 100644 index 00000000000..0083d52d2f6 --- /dev/null +++ b/ts/components/countdown/CountdownProvider.tsx @@ -0,0 +1,58 @@ +import React, { + createContext, + useContext, + useState, + ReactNode, + useRef +} from "react"; +import BackgroundTimer from "react-native-background-timer"; + +type CountdownContextType = { + timerCount: number; + resetTimer?: () => void; + startTimer?: () => void; + isRunning?: () => boolean; +}; + +const CountdownContext = createContext({ timerCount: 0 }); + +// Props type for the provider component +interface CountdownProviderProps { + children: ReactNode; + timerTiming: number; + intervalDuration: number; +} + +export const CountdownProvider = (props: CountdownProviderProps) => { + const { children, timerTiming, intervalDuration } = props; + const [timerCount, setTimerCount] = useState(timerTiming); + const isRunningTimer = useRef(false); + + const startTimer = () => { + // eslint-disable-next-line functional/immutable-data + isRunningTimer.current = true; + BackgroundTimer.runBackgroundTimer(() => { + setTimerCount(prevCount => (prevCount > 0 ? prevCount - 1 : 0)); + }, intervalDuration); + }; + + const resetTimer = () => { + setTimerCount(timerTiming); + BackgroundTimer.stopBackgroundTimer(); + // eslint-disable-next-line functional/immutable-data + isRunningTimer.current = false; + }; + + const isRunning = () => isRunningTimer.current; + + return ( + + {children} + + ); +}; + +// Hook to use the countdown context +export const useCountdown = () => useContext(CountdownContext); diff --git a/ts/components/cta/ExtractedCTABar.tsx b/ts/components/cta/ExtractedCTABar.tsx index 61b4e5d666b..1e9bb07f129 100644 --- a/ts/components/cta/ExtractedCTABar.tsx +++ b/ts/components/cta/ExtractedCTABar.tsx @@ -35,7 +35,7 @@ const renderCtaButton = ( if (isPNOptInMessage) { trackPNOptInMessageAccepted(); } - handleCtaAction(cta, linkTo, service); + handleCtaAction(cta, linkTo, service?.service_id); }; if (cta !== undefined && isCtaActionValid(cta, serviceMetadata)) { @@ -68,7 +68,7 @@ const ExtractedCTABar: React.FunctionComponent = ( props, linkTo, false, - props.isPNOptInMessage?.cta2HasServiceNavigationLink ?? false, + props.isPNOptInMessage?.cta2LinksToPNService ?? false, ctas.cta_2 ), [ctas.cta_2, linkTo, props] @@ -79,7 +79,7 @@ const ExtractedCTABar: React.FunctionComponent = ( props, linkTo, true, - props.isPNOptInMessage?.cta1HasServiceNavigationLink ?? false, + props.isPNOptInMessage?.cta1LinksToPNService ?? false, ctas.cta_1 ), [ctas.cta_1, linkTo, props] diff --git a/ts/components/helpers/withErrorModal.tsx b/ts/components/helpers/withErrorModal.tsx deleted file mode 100644 index 18da5d90bac..00000000000 --- a/ts/components/helpers/withErrorModal.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { Text as NBButtonText } from "native-base"; -import * as React from "react"; -import { Image, StyleSheet, View } from "react-native"; - -import { IOColors } from "@pagopa/io-app-design-system"; -import I18n from "../../i18n"; -import variables from "../../theme/variables"; -import ButtonDefaultOpacity from "../ButtonDefaultOpacity"; -import { H1 } from "../core/typography/H1"; -import { Overlay } from "../ui/Overlay"; - -const styles = StyleSheet.create({ - contentWrapper: { - flex: 1, - padding: variables.contentPadding - }, - - imageAndMessageContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center" - }, - - image: { - marginBottom: 20 - }, - - buttonsContainer: { - flexDirection: "row", - marginTop: "auto" - }, - - buttonCancel: { - flex: 4, - backgroundColor: IOColors.bluegrey - }, - - separator: { - width: 10 - }, - - buttonRetry: { - flex: 8 - } -}); - -/** - * A HOC to display a modal to notify the user an error occurred. - * Inside the modal cancel and retry buttons are rendered conditionally. - * - * @param WrappedComponent The react component you want to wrap - * @param errorSelector A redux selector that returns the error (as string) or undefined - * @param errorMapping A mapping function that converts the extracted error (if any) into a user-readable string - */ -export function withErrorModal< - E, - P extends Readonly<{ - error: O.Option; - onCancel: () => void; - onRetry?: () => void; - }> ->(WrappedComponent: React.ComponentType

, errorMapping: (t: E) => string) { - class WithErrorModal extends React.Component

{ - public render() { - const { error } = this.props; - - const errorMessage = pipe( - error, - O.fold( - () => "", - e => errorMapping(e) - ) - ); - - return ( - - - - ); - } - - private renderContent = (errorMessage: string) => ( - - - -

{errorMessage}

-
- {this.renderButtons()} - - ); - - private renderButtons = () => ( - - - - {I18n.t("global.buttons.cancel")} - - - {this.props.onRetry && } - {this.props.onRetry && ( - - {I18n.t("global.buttons.retry")} - - )} - - ); - } - - return WithErrorModal; -} diff --git a/ts/components/helpers/withItemsSelection.tsx b/ts/components/helpers/withItemsSelection.tsx deleted file mode 100644 index 1479abd7de0..00000000000 --- a/ts/components/helpers/withItemsSelection.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Omit } from "@pagopa/ts-commons/lib/types"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import hoistNonReactStatics from "hoist-non-react-statics"; -import React from "react"; - -type State = { - selectedItemIds: O.Option>; -}; - -export type InjectedWithItemsSelectionProps = { - selectedItemIds: O.Option>; - toggleItemSelection: (id: string) => void; - resetSelection: () => void; - setSelectedItemIds: (newSelectedItemIds: O.Option>) => void; -}; - -/** - * An HOC to maintain and manipulate a list of selected items. - * - * @deprecated please use the hook useItemsSelection instead - */ -export function withItemsSelection

( - WrappedComponent: React.ComponentType

-) { - class WithItemsSelection extends React.PureComponent< - Omit, - State - > { - constructor(props: Omit) { - super(props); - this.state = { - selectedItemIds: O.none - }; - } - - public render() { - const { selectedItemIds } = this.state; - return ( - - ); - } - - // A function to add/remove an id from the selectedItemIds Set. - private toggleItemSelection = (id: string) => { - this.setState(({ selectedItemIds }) => - pipe( - selectedItemIds, - O.map(_ => { - const newSelectedItemIds = new Set(_); - if (newSelectedItemIds.has(id)) { - newSelectedItemIds.delete(id); - } else { - newSelectedItemIds.add(id); - } - - return { - selectedItemIds: O.some(newSelectedItemIds) - }; - }), - O.getOrElse(() => ({ - selectedItemIds: O.some(new Set([id])) - })) - ) - ); - }; - - private setSelectedItemIds = ( - newSelectedItemIds: O.Option> - ) => { - this.setState({ - selectedItemIds: newSelectedItemIds - }); - }; - - private resetSelection = () => { - this.setState({ - selectedItemIds: O.none - }); - }; - } - - hoistNonReactStatics(WithItemsSelection, WrappedComponent); - - return WithItemsSelection; -} diff --git a/ts/components/markdown/MarkdownBaseScreen.tsx b/ts/components/markdown/MarkdownBaseScreen.tsx deleted file mode 100644 index 1ab6024f4a1..00000000000 --- a/ts/components/markdown/MarkdownBaseScreen.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from "react"; -import * as React from "react"; -import { View, StyleSheet } from "react-native"; -import BaseScreenComponent from "../screens/BaseScreenComponent"; -import { EdgeBorderComponent } from "../screens/EdgeBorderComponent"; -import ScreenContent from "../screens/ScreenContent"; -import Markdown from "../ui/Markdown"; -import themeVariables from "../../theme/variables"; - -/** - * TODO Rename the title prop in the BaseScreenComponent to navigationTitle - * https://www.pivotaltracker.com/story/show/173056117 - */ -type Props = { - markDown: string; - navigationTitle?: string; - title?: string; - subtitle?: string; - hideHeader?: boolean; -}; - -const styles = StyleSheet.create({ - markdownContainer: { - paddingLeft: themeVariables.contentPadding, - paddingRight: themeVariables.contentPadding - } -}); - -/** - * A base screen that allow the rendering of a markdown, in addition with a title and subtitle - * @param props - * @constructor - */ -export const MarkdownBaseScreen: React.FunctionComponent = props => { - const [isMarkdownLoaded, setMarkdownLoaded] = useState(false); - - useEffect(() => setMarkdownLoaded(false), [props.markDown]); - - return ( - - - - setMarkdownLoaded(true)}> - {props.markDown} - - {isMarkdownLoaded && } - - - {isMarkdownLoaded && props.children} - - ); -}; diff --git a/ts/components/screens/BaseScreenComponent/utils.tsx b/ts/components/screens/BaseScreenComponent/utils.tsx index 04ddd89cebb..27bd6d1d037 100644 --- a/ts/components/screens/BaseScreenComponent/utils.tsx +++ b/ts/components/screens/BaseScreenComponent/utils.tsx @@ -7,7 +7,7 @@ import { ScreenCHData } from "../../../../definitions/content/ScreenCHData"; import { ContextualHelpData } from "../../../features/zendesk/screens/ZendeskSupportHelpCenter"; import I18n from "../../../i18n"; import { handleItemOnPress } from "../../../utils/url"; -import Markdown from "../../ui/Markdown"; +import LegacyMarkdown from "../../ui/Markdown/LegacyMarkdown"; import { deriveCustomHandledLink, isIoInternalLink @@ -50,13 +50,13 @@ export const getContextualHelpConfig = ( : contextualHelpMarkdown ? { body: () => ( - {I18n.t(contextualHelpMarkdown.body)} - + ), title: I18n.t(contextualHelpMarkdown.title) } @@ -77,7 +77,9 @@ export const getContextualHelpData = ( () => defaultData, data => ({ title: data.title, - content: {data.content}, + content: ( + {data.content} + ), faqs: pipe( data.faqs, O.fromNullable, diff --git a/ts/components/screens/IdpCustomContextualHelpContent.tsx b/ts/components/screens/IdpCustomContextualHelpContent.tsx index 172313f4c71..321045b12f8 100644 --- a/ts/components/screens/IdpCustomContextualHelpContent.tsx +++ b/ts/components/screens/IdpCustomContextualHelpContent.tsx @@ -4,7 +4,7 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import { Idp } from "../../../definitions/content/Idp"; import { handleItemOnPress } from "../../utils/url"; import BlockButtons from "../ui/BlockButtons"; -import Markdown from "../ui/Markdown"; +import LegacyMarkdown from "../ui/Markdown/LegacyMarkdown"; import EmailCallCTA from "./EmailCallCTA"; type Props = Readonly<{ @@ -25,11 +25,11 @@ const IdpCustomContextualHelpBody: React.FunctionComponent = props => { return ( {/** Recover credentials */} - setIsMarkdown1Loaded(true)}> + setIsMarkdown1Loaded(true)}> {idpTextData.recover_username ? I18n.t("authentication.idp_login.dualRecoverDescription") : I18n.t("authentication.idp_login.recoverDescription")} - + {isMarkdown1Loaded && ( @@ -60,9 +60,9 @@ const IdpCustomContextualHelpBody: React.FunctionComponent = props => { {/** Idp contacts */} - setIsMarkdown2Loaded(true)}> + setIsMarkdown2Loaded(true)}> {idpTextData.description} - + {isMarkdown2Loaded && ( diff --git a/ts/components/screens/OperationResultScreenContent.tsx b/ts/components/screens/OperationResultScreenContent.tsx index 3d28636e18e..92d50d74702 100644 --- a/ts/components/screens/OperationResultScreenContent.tsx +++ b/ts/components/screens/OperationResultScreenContent.tsx @@ -1,25 +1,44 @@ import { + Body, ButtonLink, ButtonLinkProps, ButtonSolid, ButtonSolidProps, + ExternalTypographyProps, H3, + IOColors, + IOFontWeight, IOPictograms, IOStyles, + IOTheme, IOVisualCostants, Pictogram, + TypographyProps, VSpacer, WithTestID } from "@pagopa/io-app-design-system"; import * as React from "react"; -import { Platform, SafeAreaView, StyleSheet, View } from "react-native"; +import { Platform, StyleSheet, View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; -import { LabelSmall } from "../core/typography/LabelSmall"; +import { SafeAreaView } from "react-native-safe-area-context"; + +type PartialAllowedColors = Extract< + IOColors, + "bluegreyDark" | "white" | "blue" | "bluegrey" | "bluegreyLight" +>; +type AllowedColors = PartialAllowedColors | IOTheme["textBody-default"]; +type AllowedWeight = IOFontWeight | "Regular" | "SemiBold"; + +export type BodyProps = ExternalTypographyProps< + TypographyProps & { + text: string | React.ReactElement; + } +>; type OperationResultScreenContentProps = WithTestID<{ pictogram?: IOPictograms; title: string; - subtitle?: string; + subtitle?: string | Array; action?: Pick< ButtonSolidProps, "label" | "accessibilityLabel" | "onPress" | "testID" @@ -28,8 +47,27 @@ type OperationResultScreenContentProps = WithTestID<{ ButtonLinkProps, "label" | "accessibilityLabel" | "onPress" | "testID" >; + isHeaderVisible?: boolean; }>; +type PropsComposedBody = { + subtitle: Array; + textAlign?: "auto" | "left" | "right" | "center" | "justify" | undefined; +}; + +export const ComposedBodyFromArray = ({ + subtitle, + textAlign = "center" +}: PropsComposedBody) => ( + + {subtitle.map(({ text, key, ...props }) => ( + + {text} + + ))} + +); + const OperationResultScreenContent = ({ pictogram, title, @@ -37,9 +75,14 @@ const OperationResultScreenContent = ({ action, secondaryAction, children, - testID + testID, + isHeaderVisible }: React.PropsWithChildren) => ( - + - - {subtitle} - + {typeof subtitle === "string" ? ( + {subtitle} + ) : ( + + )} )} {action && ( @@ -79,6 +124,7 @@ const OperationResultScreenContent = ({ )} + {React.isValidElement(children) && React.cloneElement(children)} diff --git a/ts/components/screens/ScreenContentHeader.tsx b/ts/components/screens/ScreenContentHeader.tsx index 949182243e4..96e08f06cba 100644 --- a/ts/components/screens/ScreenContentHeader.tsx +++ b/ts/components/screens/ScreenContentHeader.tsx @@ -29,7 +29,7 @@ type Props = Readonly<{ subtitle?: string; subtitleLink?: JSX.Element; dark?: boolean; - dynamicHeight?: Animated.AnimatedInterpolation; + dynamicHeight?: Animated.AnimatedInterpolation; // Specified if a custom component is needed, if both icon and rightComponent are defined rightComponent // will be rendered in place of icon rightComponent?: React.ReactElement; @@ -45,8 +45,8 @@ const styles = StyleSheet.create({ } }); -const shouldCollapse = 1 as unknown as Animated.AnimatedInterpolation; -const shouldExpand = 0 as unknown as Animated.AnimatedInterpolation; +const shouldCollapse = 1 as unknown as Animated.AnimatedInterpolation; +const shouldExpand = 0 as unknown as Animated.AnimatedInterpolation; export class ScreenContentHeader extends React.PureComponent { private heightAnimation: Animated.Value; diff --git a/ts/components/screens/__tests__/__snapshots__/OperationResultScreenContent.test.tsx.snap b/ts/components/screens/__tests__/__snapshots__/OperationResultScreenContent.test.tsx.snap index 456cc47fe98..6f403769568 100644 --- a/ts/components/screens/__tests__/__snapshots__/OperationResultScreenContent.test.tsx.snap +++ b/ts/components/screens/__tests__/__snapshots__/OperationResultScreenContent.test.tsx.snap @@ -20,627 +20,670 @@ exports[`OperationResultScreenContent should match the snapshot with default pro } > - - - + + /> + + - + - WALLET_HOME - + + WALLET_HOME + + + - - - - - + - - - - - + + - title - - - + title + + + - subtitle - - + weight="Regular" + > + subtitle + - + > + /> + - - Action - + + Action + + - - - + > + /> + - - Secondary Action - + + Secondary Action + + - - - + + + - - + + diff --git a/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx b/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx index 0c49c894276..efac15ced66 100644 --- a/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx +++ b/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx @@ -5,10 +5,10 @@ import { NotificationChannelEnum } from "../../../../../definitions/backend/Noti import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import I18n from "../../../../i18n"; import { applicationChangeState } from "../../../../store/actions/application"; -import { loadServicePreference } from "../../../../store/actions/services/servicePreference"; +import { loadServicePreference } from "../../../../features/services/store/actions"; import { appReducer } from "../../../../store/reducers"; import { GlobalState } from "../../../../store/reducers/types"; -import { ServicePreferenceResponse } from "../../../../types/services/ServicePreferenceResponse"; +import { ServicePreferenceResponse } from "../../../../features/services/types/ServicePreferenceResponse"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; import ContactPreferencesToggles from "../index"; diff --git a/ts/components/services/ContactPreferencesToggles/index.tsx b/ts/components/services/ContactPreferencesToggles/index.tsx index 2d8072737d0..07e08a7f6f1 100644 --- a/ts/components/services/ContactPreferencesToggles/index.tsx +++ b/ts/components/services/ContactPreferencesToggles/index.tsx @@ -10,19 +10,19 @@ import I18n from "../../../i18n"; import { loadServicePreference, upsertServicePreference -} from "../../../store/actions/services/servicePreference"; +} from "../../../features/services/store/actions"; import { Dispatch } from "../../../store/actions/types"; import { useIOSelector } from "../../../store/hooks"; import { isPremiumMessagesOptInOutEnabledSelector } from "../../../store/reducers/backendStatus"; import { servicePreferenceSelector, ServicePreferenceState -} from "../../../store/reducers/entities/services/servicePreference"; +} from "../../../features/services/store/reducers/servicePreference"; import { GlobalState } from "../../../store/reducers/types"; import { isServicePreferenceResponseSuccess, ServicePreference -} from "../../../types/services/ServicePreferenceResponse"; +} from "../../../features/services/types/ServicePreferenceResponse"; import { isStrictSome } from "../../../utils/pot"; import { showToast } from "../../../utils/showToast"; import ItemSeparatorComponent from "../../ItemSeparatorComponent"; diff --git a/ts/components/services/LocalServicesWebView.tsx b/ts/components/services/LocalServicesWebView.tsx index 9f56276605d..3fa9fdebb97 100644 --- a/ts/components/services/LocalServicesWebView.tsx +++ b/ts/components/services/LocalServicesWebView.tsx @@ -14,7 +14,7 @@ import { localServicesWebUrl } from "../../config"; import { useTabItemPressWhenScreenActive } from "../../hooks/useTabItemPressWhenScreenActive"; import I18n from "../../i18n"; import { loadServiceDetail } from "../../store/actions/services"; -import { servicesByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { servicesByIdSelector } from "../../features/services/store/reducers/servicesById"; import { GlobalState } from "../../store/reducers/types"; import { isStrictSome } from "../../utils/pot"; import { showToast } from "../../utils/showToast"; diff --git a/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap b/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap index 8213c9a3f98..b9c16fdd5f8 100644 --- a/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap +++ b/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap @@ -17,7 +17,6 @@ exports[`SectionHeader component should match the snapshot 1`] = ` align="xMidYMid" bbHeight={20} bbWidth={20} - color={4282866285} focusable={false} height={20} importantForAccessibility="no-hide-descendants" @@ -40,19 +39,26 @@ exports[`SectionHeader component should match the snapshot 1`] = ` }, ] } - tintColor={4282866285} + tintColor="#475A6D" vbHeight={24} vbWidth={24} width={20} > - + ["onLinkClicked"]; - shouldHandleLink?: ComponentProps["shouldHandleLink"]; + onLinkClicked?: ComponentProps["onLinkClicked"]; + shouldHandleLink?: ComponentProps["shouldHandleLink"]; }; const styles = StyleSheet.create({ @@ -34,7 +34,7 @@ const styles = StyleSheet.create({ * * @param props * @constructor - * @deprecated Please use {@link RawAccordion} or {@link IOAccordion} + * @deprecated Please use {@link RawAccordion} */ const Accordion: React.FunctionComponent = (props: Props) => { const [expanded, setExpanded] = React.useState(false); @@ -69,7 +69,7 @@ const Accordion: React.FunctionComponent = (props: Props) => { const renderContent = (content: string) => ( - { pipe( @@ -80,7 +80,7 @@ const Accordion: React.FunctionComponent = (props: Props) => { }} > {content} - + ); diff --git a/ts/components/ui/DateTimePicker.tsx b/ts/components/ui/DateTimePicker.tsx deleted file mode 100644 index fe05df77a2b..00000000000 --- a/ts/components/ui/DateTimePicker.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react"; -import DateTimePickerModal from "react-native-modal-datetime-picker"; -import { useState } from "react"; -import { StyleSheet, View } from "react-native"; -import { IOColors, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import { H5 } from "../core/typography/H5"; -import { H4 } from "../core/typography/H4"; -import { formatDateAsLocal } from "../../utils/dates"; -import TouchableDefaultOpacity from "../TouchableDefaultOpacity"; -import I18n from "../../i18n"; - -type Props = { - date: Date | undefined; - onConfirm: (date: Date) => void; - label?: string; - minimumDate?: Date; - blocked?: boolean; -}; - -const styles = StyleSheet.create({ - container: { borderBottomWidth: 1, borderColor: IOColors.bluegreyLight }, - inputContainer: { - flex: 1, - flexDirection: "row", - justifyContent: "space-between" - } -}); - -const DateTimePicker: React.FunctionComponent = (props: Props) => { - const [showModal, setShowModal] = useState(false); - - const onConfirm = (date: Date) => { - props.onConfirm(date); - setShowModal(false); - }; - - const onDismiss = () => setShowModal(false); - - const onPress = () => { - if (props.blocked !== true) { - setShowModal(true); - } - }; - - return ( - - {props.label &&

{props.label}
} - - -

- {props.date - ? formatDateAsLocal(props.date, true, true) - : I18n.t("global.dateFormats.dateTimePicker")} -

- -
- - -
- ); -}; - -export default DateTimePicker; diff --git a/ts/components/ui/LoadingIndicator.tsx b/ts/components/ui/LoadingIndicator.tsx index e41d9debb9a..857bb04ac61 100644 --- a/ts/components/ui/LoadingIndicator.tsx +++ b/ts/components/ui/LoadingIndicator.tsx @@ -1,10 +1,8 @@ import * as React from "react"; -import { - LoadingSpinner, - useIOExperimentalDesign -} from "@pagopa/io-app-design-system"; +import { LoadingSpinner } from "@pagopa/io-app-design-system"; import I18n from "i18n-js"; import { WithTestID } from "../../types/WithTestID"; +import { useInteractiveElementDefaultColor } from "../../utils/hooks/theme"; export type LoadingIndicator = WithTestID< Exclude< @@ -18,14 +16,14 @@ export const LoadingIndicator = ({ accessibilityLabel = I18n.t("global.accessibility.activityIndicator.label"), testID = "LoadingIndicator" }: LoadingIndicator) => { - const { isExperimental } = useIOExperimentalDesign(); + const blueColor = useInteractiveElementDefaultColor(); return ( ); diff --git a/ts/components/ui/LogoPaymentExtended.tsx b/ts/components/ui/LogoPaymentExtended.tsx index b53119ddd0c..241891ae9a7 100644 --- a/ts/components/ui/LogoPaymentExtended.tsx +++ b/ts/components/ui/LogoPaymentExtended.tsx @@ -3,7 +3,7 @@ import { Image } from "react-native"; import paypalLogoImage from "../../../img/wallet/payment-methods/paypal-logo.png"; // sadly no svg is available for paypal, since on Figma an image is used import BpayLogo from "../../../img/wallet/payment-methods/bpay_logo_full.svg"; -import { BankLogoOrSkeleton } from "./utils/components/BankLogoOrLoadingSkeleton"; +import { BankLogoOrSkeleton } from "../../features/payments/common/components/utils/BankLogoOrLoadingSkeleton"; export type LogoPaymentExtendedProps = { dimensions: { height: number; width: number }; } & ( diff --git a/ts/components/ui/Markdown/index.tsx b/ts/components/ui/Markdown/LegacyMarkdown.tsx similarity index 97% rename from ts/components/ui/Markdown/index.tsx rename to ts/components/ui/Markdown/LegacyMarkdown.tsx index 4e461a6a7f7..ae08a6edc4d 100644 --- a/ts/components/ui/Markdown/index.tsx +++ b/ts/components/ui/Markdown/LegacyMarkdown.tsx @@ -25,7 +25,7 @@ import customVariables from "../../../theme/variables"; import { WithTestID } from "../../../types/WithTestID"; import { remarkProcessor } from "../../../utils/markdown"; import { closeInjectedScript } from "../../../utils/webview"; -import MarkdownWebviewComponent from "./MarkdownWebviewComponent"; +import { MarkdownWebviewComponent } from "./MarkdownWebviewComponent"; import { NOTIFY_BODY_HEIGHT_SCRIPT, NOTIFY_LINK_CLICK_SCRIPT } from "./script"; const INJECTED_JAVASCRIPT = ` @@ -208,7 +208,7 @@ type State = { /** * A component to render the message markdown as HTML inside a WebView */ -class Markdown extends React.PureComponent { +class LegacyMarkdown extends React.PureComponent { private webViewRef = React.createRef(); private subscription: NativeEventSubscription | undefined; @@ -398,6 +398,6 @@ class Markdown extends React.PureComponent { }; } -export type MarkdownProps = OwnProps; +export type LegacyMarkdownProps = OwnProps; -export default connect()(Markdown); +export default connect()(LegacyMarkdown); diff --git a/ts/components/ui/Markdown/LoadingSkeleton.tsx b/ts/components/ui/Markdown/LoadingSkeleton.tsx new file mode 100644 index 00000000000..88912d8b6e1 --- /dev/null +++ b/ts/components/ui/Markdown/LoadingSkeleton.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { View } from "react-native"; +import Placeholder from "rn-placeholder"; +import { VSpacer } from "@pagopa/io-app-design-system"; +import I18n from "../../../i18n"; + +type LoadingSkeletonProps = { + testID?: string; +}; + +export const LoadingSkeleton = ({ testID }: LoadingSkeletonProps) => ( + + + + + + + + + + + + + + + + + + + + + +); diff --git a/ts/components/ui/Markdown/Markdown.tsx b/ts/components/ui/Markdown/Markdown.tsx new file mode 100644 index 00000000000..6aca1b25ef5 --- /dev/null +++ b/ts/components/ui/Markdown/Markdown.tsx @@ -0,0 +1,196 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + AppState, + AppStateStatus, + InteractionManager, + LayoutAnimation, + Platform, + ScrollView, + StyleProp, + UIManager, + View, + ViewStyle +} from "react-native"; +import WebView from "react-native-webview"; +import { closeInjectedScript } from "../../../utils/webview"; +import { remarkProcessor } from "../../../utils/markdown"; +import { MarkdownWebviewComponent } from "./MarkdownWebviewComponent"; +import { NOTIFY_BODY_HEIGHT_SCRIPT, NOTIFY_LINK_CLICK_SCRIPT } from "./script"; +import { LoadingSkeleton } from "./LoadingSkeleton"; +import { convertOldDemoMarkdownTag, generateHtml } from "./utils"; + +export type MarkdownProps = { + animated?: boolean; + /** + * The code will be inserted in the html body between + * tags. + */ + avoidTextSelection?: true; + children: string; + cssStyle?: string; + extraBodyHeight?: number; + letUserZoom?: boolean; + onError?: (error: any) => void; + onLinkClicked?: (url: string) => void; + onLoadEnd?: () => void; + /** + * if shouldHandleLink returns true the clicked link will be handled by the Markdown component + * otherwise Markdown will ignore it. If shouldHandleLink is not defined assume () => true + * @param url + */ + shouldHandleLink?: (url: string) => boolean; + testID?: string; + webViewStyle?: StyleProp; + useCustomSortedList?: boolean; +}; + +type InternalState = { + html?: string; + htmlBodyHeight: number; + isLoading: boolean; + webviewKey: number; +}; + +export const Markdown = (props: MarkdownProps) => { + const [internalState, setInternalState] = useState({ + html: undefined, + htmlBodyHeight: 0, + isLoading: true, + webviewKey: 0 + }); + const webViewRef = useRef(null); + const { html, htmlBodyHeight, isLoading, webviewKey } = internalState; + const containerStyle: ViewStyle = { + height: htmlBodyHeight + (props.extraBodyHeight || 0) + }; + const handleLoadEnd = useCallback(() => { + props.onLoadEnd?.(); + setTimeout(() => { + // to avoid yellow box warning + // it's ugly but it works https://github.com/react-native-community/react-native-webview/issues/341#issuecomment-466639820 + webViewRef.current?.injectJavaScript( + closeInjectedScript(NOTIFY_BODY_HEIGHT_SCRIPT) + ); + }, 100); + }, [props]); + + const compileMarkdownAsync = useCallback( + ( + markdown: string, + animated: boolean = false, + onError?: (error: any) => void, + cssStyle?: string, + useCustomSortedList: boolean = false, + avoidTextSelection: boolean = false + ) => { + void InteractionManager.runAfterInteractions(() => { + if (animated) { + // Animate the layout change + // See https://facebook.github.io/react-native/docs/layoutanimation.html + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + } + remarkProcessor.process( + convertOldDemoMarkdownTag( + // sanitize html to prevent xss attacks + filterXSS(markdown, { stripIgnoreTagBody: ["script"] }) + ), + (error: any, file: any) => { + if (error) { + onError?.(error); + return; + } + // The check below on `isLoading` is to set the property back to 'false' + // value when refreshing with metro, since it is set back to the initial + // 'true' value but the underlying MarkdownWebviewComponent does not + // reload its content (the html is recompiled but it does not change), + // thus, not calling the `handleLoadEnd` callback + const html = generateHtml( + String(file), + cssStyle, + useCustomSortedList, + avoidTextSelection + ); + setInternalState(currentInternalState => ({ + ...currentInternalState, + isLoading: + currentInternalState.isLoading && + currentInternalState.html !== html, + html + })); + } + ); + }); + }, + [] + ); + + useEffect(() => { + setInternalState(currentInternalState => ({ + ...currentInternalState, + isLoading: true + })); + compileMarkdownAsync( + props.children, + props.animated, + props.onError, + props.cssStyle, + props.useCustomSortedList, + props.avoidTextSelection + ); + + const subscription = AppState.addEventListener( + "change", + (nextAppState: AppStateStatus) => { + if (Platform.OS === "ios" && nextAppState === "active") { + // Reloads the WebView on iOS. Using reload() on the webview + // reference causes the injected javascript to fail + setInternalState(currentInternalState => ({ + ...currentInternalState, + webviewKey: currentInternalState.webviewKey + 1 + })); + } + } + ); + return () => subscription.remove(); + }, [compileMarkdownAsync, props]); + + return ( + <> + {isLoading && } + {/* Hide the WebView until we have the htmlBodyHeight */} + {html && ( + + + + setInternalState(currentInternalState => ({ + ...currentInternalState, + isLoading: false + })) + } + setHtmlBodyHeight={(inputHtmlBodyHeight: number) => + setInternalState(currentInternalState => ({ + ...currentInternalState, + htmlBodyHeight: inputHtmlBodyHeight + })) + } + webViewStyle={props.webViewStyle} + onLinkClicked={props.onLinkClicked} + letUserZoom={props.letUserZoom} + testID={props.testID} + /> + + + )} + + ); +}; diff --git a/ts/components/ui/Markdown/MarkdownWebviewComponent.tsx b/ts/components/ui/Markdown/MarkdownWebviewComponent.tsx index 6dc1611c090..75be6f68349 100644 --- a/ts/components/ui/Markdown/MarkdownWebviewComponent.tsx +++ b/ts/components/ui/Markdown/MarkdownWebviewComponent.tsx @@ -2,7 +2,7 @@ import { useLinkTo } from "@react-navigation/native"; import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import React from "react"; +import React, { LegacyRef } from "react"; import { StyleProp, ViewStyle } from "react-native"; import { WebView } from "react-native-webview"; import { WebViewMessageEvent } from "react-native-webview/lib/WebViewTypes"; @@ -16,7 +16,7 @@ type Props = { handleLoadEnd: () => void; html: string; webviewKey: number; - webViewRef: React.RefObject; + webViewRef: LegacyRef; setLoadingFalse: () => void; setHtmlBodyHeight: (h: number) => void; shouldHandleLink?: (link: string) => boolean; @@ -26,7 +26,7 @@ type Props = { testID?: string; }; -const MarkdownWebviewComponent = (props: Props) => { +export const MarkdownWebviewComponent = (props: Props) => { const linkTo = useLinkTo(); const handleWebViewMessage = (event: WebViewMessageEvent) => { @@ -65,7 +65,6 @@ const MarkdownWebviewComponent = (props: Props) => { }) ); }; - return ( { /> ); }; -export default MarkdownWebviewComponent; diff --git a/ts/components/ui/Markdown/utils.ts b/ts/components/ui/Markdown/utils.ts new file mode 100644 index 00000000000..75ca07c1db9 --- /dev/null +++ b/ts/components/ui/Markdown/utils.ts @@ -0,0 +1,146 @@ +import { FontWeight, IOColors } from "@pagopa/io-app-design-system"; +import { Platform } from "react-native"; +import * as RNFS from "react-native-fs"; + +const textColor = IOColors.bluegrey; +const fontSizeBase = 16; +const textLinkWeight = "600" as FontWeight; +const textMessageDetailLinkColor = IOColors.blue; +const toastColor = IOColors.aquaUltraLight; +const brandPrimary = IOColors.blue; + +const OLD_DEMO_TAG_MARKDOWN_REGEX = /^\[demo\]([\s\S]+?)\[\/demo\]\s*\n{2,}/; +export const convertOldDemoMarkdownTag = (markdown: string) => + markdown.replace( + OLD_DEMO_TAG_MARKDOWN_REGEX, + (_, g1: string) => `::div[${g1}]{.io-demo-block}\n` + ); + +export const generateHtml = ( + content: string, + cssStyle?: string, + useCustomSortedList: boolean = false, + avoidTextSelection: boolean = false +) => ` + + + + + + + ${GLOBAL_CSS} + ${cssStyle ? generateInlineCss(cssStyle) : ""} + ${avoidTextSelection ? avoidTextSelectionCSS : ""} + ${useCustomSortedList ? generateCustomFontList : ""} + ${content} + + + `; + +const generateInlineCss = (cssStyle: string) => ``; + +const TITILLIUM_SANSPRO_FONT_PATH = + Platform.OS === "android" + ? "file:///android_asset/fonts/TitilliumSansPro-Regular.otf" + : `${RNFS.MainBundlePath}/TitilliumSansPro-Regular.otf`; + +const TITILLIUM_SANSPRO_BOLD_FONT_PATH = + Platform.OS === "android" + ? "file:///android_asset/fonts/TitilliumSansPro-Bold.otf" + : `${RNFS.MainBundlePath}/TitilliumSansPro-Bold.otf`; + +const GLOBAL_CSS = ` + +`; + +const avoidTextSelectionCSS = ``; + +const generateCustomFontList = ``; diff --git a/ts/components/ui/ProgressIndicator.tsx b/ts/components/ui/ProgressIndicator.tsx new file mode 100644 index 00000000000..646aedffce9 --- /dev/null +++ b/ts/components/ui/ProgressIndicator.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import { ProgressLoader } from "@pagopa/io-app-design-system"; +import { useInteractiveElementDefaultColor } from "../../utils/hooks/theme"; + +export type ProgressIndicator = Exclude< + React.ComponentProps, + "color" +>; + +export const ProgressIndicator = (props: ProgressIndicator) => { + const blueColor = useInteractiveElementDefaultColor(); + + return ; +}; diff --git a/ts/components/ui/RNavScreenWithLargeHeader.tsx b/ts/components/ui/RNavScreenWithLargeHeader.tsx index 11e012f0174..59320a8de5d 100644 --- a/ts/components/ui/RNavScreenWithLargeHeader.tsx +++ b/ts/components/ui/RNavScreenWithLargeHeader.tsx @@ -22,14 +22,20 @@ import { import { SupportRequestParams } from "../../hooks/useStartSupportRequest"; import I18n from "../../i18n"; +export type LargeHeaderTitleProps = { + label: string; + accessibilityLabel?: string; + testID?: string; +}; + type Props = { children: React.ReactNode; fixedBottomSlot?: React.ReactNode; - title: string; - titleTestID?: string; + title: LargeHeaderTitleProps; description?: string; goBack?: BackProps["goBack"]; headerActionsProp?: HeaderActionProps; + canGoback?: boolean; } & SupportRequestParams; /** @@ -44,13 +50,14 @@ type Props = { * @param contextualHelpMarkdown * @param faqCategories * @param headerProps + * @param canGoback allows to show/not show the back button and consequently does not pass to the HeaderSecondLevel the props that would display the back button */ export const RNavScreenWithLargeHeader = ({ children, fixedBottomSlot, title, - titleTestID, goBack, + canGoback = true, description, contextualHelp, contextualHelpMarkdown, @@ -72,10 +79,8 @@ export const RNavScreenWithLargeHeader = ({ translationY.value = event.contentOffset.y; }); - const headerProps: ComponentProps = useHeaderProps({ - backAccessibilityLabel: I18n.t("global.buttons.back"), - goBack: goBack ?? navigation.goBack, - title, + const headerPropsWithoutGoBack = { + title: title.label, scrollValues: { contentOffsetY: translationY, triggerOffset: titleHeight @@ -84,7 +89,17 @@ export const RNavScreenWithLargeHeader = ({ contextualHelpMarkdown, faqCategories, ...headerActionsProp - }); + }; + + const headerProps: ComponentProps = useHeaderProps( + canGoback + ? { + ...headerPropsWithoutGoBack, + backAccessibilityLabel: I18n.t("global.buttons.back"), + goBack: goBack ?? navigation.goBack + } + : headerPropsWithoutGoBack + ); useLayoutEffect(() => { navigation.setOptions({ @@ -109,7 +124,13 @@ export const RNavScreenWithLargeHeader = ({ style={IOStyles.horizontalContentPadding} onLayout={getTitleHeight} > -

{title}

+

+ {title.label} +

{description && ( diff --git a/ts/components/ui/TextboxWithSuggestion.tsx b/ts/components/ui/TextboxWithSuggestion.tsx deleted file mode 100644 index 8c72e2d690d..00000000000 --- a/ts/components/ui/TextboxWithSuggestion.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import * as React from "react"; -import { ReactNode, useContext, useState } from "react"; -import { Body, Container, Content, Left, Right } from "native-base"; -import { View, StyleSheet, SafeAreaView } from "react-native"; -import { IOColors, Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; -import { H4 } from "../core/typography/H4"; -import { H5 } from "../core/typography/H5"; -import TouchableDefaultOpacity from "../TouchableDefaultOpacity"; -import { IOStyles } from "../core/variables/IOStyles"; -import { LabelledItem } from "../LabelledItem"; -import ButtonDefaultOpacity from "../ButtonDefaultOpacity"; -import AppHeader from "./AppHeader"; -import { LightModalContext } from "./LightModal"; - -const styles = StyleSheet.create({ - container: { - borderBottomWidth: 1, - paddingBottom: 10 - }, - inputContainer: { - flexDirection: "row", - justifyContent: "space-between" - } -}); - -/** - * Props explanation: - * - onChangeText -> method that notify when the user changes the searching text in the modal. - * - title -> the title of the modal screen. - * - label -> displayed on top the search text-box both. - * - placeholder -> placeholder of the search text-box. - * - showModalInputTextbox -> boolean that control the input textbox in the modal - * - wrappedFlatlist -> the Flatlist component that show the suggestions that the user can choose from. Note that if the list needs to react - * and change the showed data when the text change a connected component that wrap the Flatlist is needed. - */ -type CommonProps = { - onChangeText?: (value: string) => void; - title: string; - label: string; - placeholder: string; - showModalInputTextbox: boolean; - wrappedFlatlist: ReactNode; -}; - -/** - * Props explanation: - * - selectedValue -> value to show in the initial textbox. - * - disabled -> if true disable the onPress on the initial textBox and show the border light gray - */ -type Props = { - selectedValue?: string; - disabled?: boolean; - onClose?: () => void; -} & CommonProps; - -type ModalProps = { - onClose: () => void; -} & CommonProps; - -const TextboxWithSuggestionModal = (props: ModalProps) => { - const [inputValue, setInputValue] = useState(""); - return ( - - - - - - - - -
- {props.title} -
- - -
- - - {props.showModalInputTextbox && ( - <> - { - setInputValue(v); - props.onChangeText?.(v); - }, - placeholder: props.placeholder - }} - /> - - - )} - {props.wrappedFlatlist} - - -
- ); -}; - -/** - * This component is a wrapper around a modal that contains: - * - a LabelledItem component - * - a Flatlist that shows a list of results from which the user has to choose - * A callback is available: - * - onChangeText -> executed when the user inserts or cancels some character from the LabelledItem - * @param props - * @constructor - */ -const TextboxWithSuggestion = (props: Props) => { - const { showModal, hideModal } = useContext(LightModalContext); - - const borderBottomColor = props.disabled - ? IOColors.bluegreyLight - : IOColors.bluegreyDark; - return ( - <> - - -
{props.label}
- -
- - - showModal( - { - hideModal(); - props.onClose?.(); - }} - onChangeText={props.onChangeText} - showModalInputTextbox={props.showModalInputTextbox} - wrappedFlatlist={props.wrappedFlatlist} - /> - ) - } - > - {props.selectedValue ? ( -

- {props.selectedValue} -

- ) : ( -

- {props.placeholder} -

- )} -
-
- - ); -}; - -export default TextboxWithSuggestion; diff --git a/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap b/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap index a0c0c9aebab..bce0f1fd1fe 100644 --- a/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap +++ b/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap @@ -113,6 +113,12 @@ exports[`BoxedRefreshIndicator Should match all-properties snapshot 1`] = ` /> @@ -284,6 +313,12 @@ exports[`BoxedRefreshIndicator Should match base snapshot 1`] = ` /> diff --git a/ts/components/ui/__test__/__snapshots__/TabItem.test.tsx.snap b/ts/components/ui/__test__/__snapshots__/TabItem.test.tsx.snap index 48f7f0caaae..e805818fb1a 100644 --- a/ts/components/ui/__test__/__snapshots__/TabItem.test.tsx.snap +++ b/ts/components/ui/__test__/__snapshots__/TabItem.test.tsx.snap @@ -49,7 +49,6 @@ exports[`TabItem should match the snapshot with dark color 1`] = ` align="xMidYMid" bbHeight={16} bbWidth={16} - color={4294967295} focusable={false} height={16} importantForAccessibility="no-hide-descendants" @@ -72,19 +71,26 @@ exports[`TabItem should match the snapshot with dark color 1`] = ` }, ] } - tintColor={4294967295} + tintColor="#FFFFFF" vbHeight={24} vbWidth={24} width={16} > - + - + - + - + - - - - - - - - - - - - WALLET_HOME - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`PaymentCardBig component matches snapshot for paypal 1`] = ` - - - - - - - - - - - - - WALLET_HOME - - - - - - - - - - - - - - - - - - - - - someEmail@test.com - - - - - - - - - - - - -`; - -exports[`PaymentCardSmall component matches snapshot for loading 1`] = ` - - - - - - - - - - - - - WALLET_HOME - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`PaymentCardSmall component matches snapshot for paypal 1`] = ` - - - - - - - - - - - - - WALLET_HOME - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PayPal - - - - - - - - - - - - -`; diff --git a/ts/components/ui/utils/components/PressableListItemBase.tsx b/ts/components/ui/utils/components/PressableListItemBase.tsx deleted file mode 100644 index 08d32210ca6..00000000000 --- a/ts/components/ui/utils/components/PressableListItemBase.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { Pressable } from "react-native"; -import Animated from "react-native-reanimated"; -import { IOListItemStyles } from "@pagopa/io-app-design-system"; -import { WithTestID } from "../../../../types/WithTestID"; -import { useListItemBaseSpringAnimation } from "../hooks/useListItemBaseSpringAnimation"; - -export type PressableBaseProps = WithTestID<{ - accessibilityLabel?: string; - onPress?: () => void; -}>; - -/** - * A base component for creating pressable list items with animation support. - * - * @param {string} accessibilityLabel - An optional label for accessibility. - * @param {function} onPress - The function to be executed when the item is pressed. - * @param {string} testID - An optional test identifier for testing purposes. - * @param {React.ReactNode} children - The content to be rendered within the list item. - * - * @deprecated The usage of this component is discouraged as it is being replaced by the PressableListItemBase of the @pagopa/io-app-design-system library. - * - */ -export const PressableListItemBase = ({ - onPress, - testID, - accessibilityLabel, - children -}: React.PropsWithChildren) => { - const { onPressIn, onPressOut, animatedScaleStyle, animatedBackgroundStyle } = - useListItemBaseSpringAnimation(); - return ( - - - - {children} - - - - ); -}; diff --git a/ts/components/wallet/PagoPALogo.tsx b/ts/components/wallet/PagoPALogo.tsx deleted file mode 100644 index 28b31684287..00000000000 --- a/ts/components/wallet/PagoPALogo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Render the pagoPA Logo according to the environment (Test or Production) - */ -import * as React from "react"; -import { Image } from "react-native"; -import { connect } from "react-redux"; -import { isPagoPATestEnabledSelector } from "../../store/reducers/persistedPreferences"; -import { GlobalState } from "../../store/reducers/types"; - -type Props = ReturnType; - -class PagoPALogo extends React.Component { - public render(): React.ReactNode { - const { isPagoPATestEnabled } = this.props; - return isPagoPATestEnabled ? ( - - ) : ( - - ); - } -} - -const mapStateToProps = (state: GlobalState) => ({ - isPagoPATestEnabled: isPagoPATestEnabledSelector(state) -}); - -export default connect(mapStateToProps)(PagoPALogo); diff --git a/ts/components/wallet/PaymentsHistoryList.tsx b/ts/components/wallet/PaymentsHistoryList.tsx index 68e61ce82f6..8e49001fc91 100644 --- a/ts/components/wallet/PaymentsHistoryList.tsx +++ b/ts/components/wallet/PaymentsHistoryList.tsx @@ -136,7 +136,9 @@ export default class PaymentHistoryList extends React.Component { ItemSeparatorComponent={() => ( )} - ListFooterComponent={payments.length > 0 && } + ListFooterComponent={ + payments.length > 0 ? : null + } keyExtractor={(_, index) => index.toString()} /> diff --git a/ts/components/wallet/TransactionsList.tsx b/ts/components/wallet/TransactionsList.tsx index 991f3c5d623..b2388d525cf 100644 --- a/ts/components/wallet/TransactionsList.tsx +++ b/ts/components/wallet/TransactionsList.tsx @@ -80,13 +80,15 @@ export const TransactionsList = (props: Props) => { !areMoreTransactionsAvailable && ListEmptyComponent !== undefined; - const footerListComponent = (transactions: ReadonlyArray) => { + const footerListComponent = ( + transactions: ReadonlyArray + ): React.ComponentProps["ListFooterComponent"] => { if (!areMoreTransactionsAvailable) { - return transactions.length > 0 && ; + return transactions.length > 0 ? : null; } return ( - + <> { - + ); }; diff --git a/ts/components/wallet/creditCardOnboardingAttempts/CreditCardAttemptsList.tsx b/ts/components/wallet/creditCardOnboardingAttempts/CreditCardAttemptsList.tsx index ae0162abf91..c5880589a2b 100644 --- a/ts/components/wallet/creditCardOnboardingAttempts/CreditCardAttemptsList.tsx +++ b/ts/components/wallet/creditCardOnboardingAttempts/CreditCardAttemptsList.tsx @@ -22,7 +22,9 @@ type Props = Readonly<{ title: string; creditCardAttempts: CreditCardInsertionState; onAttemptPress: (attempt: CreditCardInsertion) => void; - ListEmptyComponent: React.ReactNode; + ListEmptyComponent: React.ComponentProps< + typeof FlatList + >["ListEmptyComponent"]; }>; const styles = StyleSheet.create({ @@ -149,7 +151,7 @@ export const CreditCardAttemptsList: React.FC = (props: Props) => { )} ListFooterComponent={ - creditCardAttempts.length > 0 && + creditCardAttempts.length > 0 ? : null } keyExtractor={c => c.hashedPan} /> diff --git a/ts/config.ts b/ts/config.ts index a7708fd2bc0..324af61d757 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -106,7 +106,7 @@ export const nativeLoginEnabled = Config.NATIVE_LOGIN_ENABLED === "YES"; // Opt-in for reminder push notifications export const remindersOptInEnabled = Config.REMINDERS_OPT_IN_ENABLED === "YES"; -export const isNewCduFlow = Config.CDU_NEW_FLOW === "YES"; +export const isNewServicesEnabled = Config.NEW_SERVICES_ENABLED === "YES"; export const fetchTimeout = pipe( parseInt(Config.FETCH_TIMEOUT_MS, 10), diff --git a/ts/features/barcode/screens/BarcodeScanScreen.tsx b/ts/features/barcode/screens/BarcodeScanScreen.tsx index b5d54f90311..4094bc40b59 100644 --- a/ts/features/barcode/screens/BarcodeScanScreen.tsx +++ b/ts/features/barcode/screens/BarcodeScanScreen.tsx @@ -1,11 +1,15 @@ -import { Divider, ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; +import { + Divider, + ListItemNav, + VSpacer, + IOToast +} from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; import React from "react"; import { Alert, View } from "react-native"; import ReactNativeHapticFeedback, { HapticFeedbackTypes } from "react-native-haptic-feedback"; -import { IOToast } from "../../../components/Toast"; import { useOpenDeepLink } from "../../../hooks/useOpenDeepLink"; import I18n from "../../../i18n"; import { mixpanelTrack } from "../../../mixpanel"; @@ -23,7 +27,7 @@ import { import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet"; import { IDPayPaymentRoutes } from "../../idpay/payment/navigation/navigator"; -import { WalletPaymentRoutes } from "../../walletV3/payment/navigation/routes"; +import { PaymentsCheckoutRoutes } from "../../payments/checkout/navigation/routes"; import * as analytics from "../analytics"; import { BarcodeScanBaseScreenComponent } from "../components/BarcodeScanBaseScreenComponent"; import { useIOBarcodeFileReader } from "../hooks/useIOBarcodeFileReader"; @@ -38,7 +42,7 @@ import { } from "../types/IOBarcode"; import { BarcodeFailure } from "../types/failure"; import { getIOBarcodesByType } from "../utils/getBarcodesByType"; -import { WalletBarcodeRoutes } from "../../walletV3/barcode/navigation/routes"; +import { PaymentsBarcodeRoutes } from "../../payments/barcode/navigation/routes"; import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton"; const BarcodeScanScreen = () => { @@ -92,8 +96,8 @@ const BarcodeScanScreen = () => { void mixpanelTrack("WALLET_SCAN_POSTE_DATAMATRIX_SUCCESS"); } - navigation.navigate(WalletBarcodeRoutes.WALLET_BARCODE_MAIN, { - screen: WalletBarcodeRoutes.WALLET_BARCODE_CHOICE, + navigation.navigate(PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR, { + screen: PaymentsBarcodeRoutes.PAYMENT_BARCODE_CHOICE, params: { barcodes: pagoPABarcodes } @@ -172,8 +176,8 @@ const BarcodeScanScreen = () => { const handlePagoPACodeInput = () => { manualInputModal.dismiss(); - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_INPUT_NOTICE_NUMBER + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_INPUT_NOTICE_NUMBER }); }; diff --git a/ts/features/bonus/cdc/analytics/index.ts b/ts/features/bonus/cdc/analytics/index.ts index e208a0f2f58..08a032da3e9 100644 --- a/ts/features/bonus/cdc/analytics/index.ts +++ b/ts/features/bonus/cdc/analytics/index.ts @@ -1,6 +1,6 @@ import { getType } from "typesafe-actions"; import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; +import { constVoid, pipe } from "fp-ts/lib/function"; import { cdcEnabled } from "../../../../config"; import { mixpanel } from "../../../../mixpanel"; import { Action } from "../../../../store/actions/types"; @@ -11,7 +11,7 @@ import { const trackCdc = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(cdcRequestBonusList.request): case getType(cdcRequestBonusList.success): @@ -44,10 +44,9 @@ const trackCdc = return mp.track(action.type, { status: action.payload.kind, value }); } - return Promise.resolve(); }; const emptyTracking = (_: NonNullable) => (__: Action) => - Promise.resolve(); + constVoid(); export default cdcEnabled ? trackCdc : emptyTracking; diff --git a/ts/features/bonus/cdc/components/CdcServiceCTA.tsx b/ts/features/bonus/cdc/components/CdcServiceCTA.tsx index 6734081f3a6..8f97a1107ad 100644 --- a/ts/features/bonus/cdc/components/CdcServiceCTA.tsx +++ b/ts/features/bonus/cdc/components/CdcServiceCTA.tsx @@ -1,6 +1,6 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useFocusEffect, useNavigation } from "@react-navigation/native"; +import { useFocusEffect } from "@react-navigation/native"; import * as React from "react"; import { useCallback } from "react"; import { View } from "react-native"; @@ -24,12 +24,13 @@ import { CDC_ROUTES } from "../navigation/routes"; import { cdcRequestBonusList } from "../store/actions/cdcBonusRequest"; import { cdcBonusRequestListSelector } from "../store/reducers/cdcBonusRequest"; import { CdcBonusRequestList } from "../types/CdcBonusRequest"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; type ReadyButtonProp = { bonusRequestList: CdcBonusRequestList; }; const ReadyButton = (props: ReadyButtonProp) => { - const navigation = useNavigation(); + const navigation = useIONavigation(); // Check if at least one year can be activable const activableBonuses = props.bonusRequestList.filter( diff --git a/ts/features/bonus/cdc/navigation/CdcStackNavigator.tsx b/ts/features/bonus/cdc/navigation/CdcStackNavigator.tsx index 779732812ff..265aef68509 100644 --- a/ts/features/bonus/cdc/navigation/CdcStackNavigator.tsx +++ b/ts/features/bonus/cdc/navigation/CdcStackNavigator.tsx @@ -13,8 +13,7 @@ const Stack = createStackNavigator(); export const CdcStackNavigator = () => ( > => [ @@ -80,7 +79,7 @@ const CdcBonusRequestSelectResidence = () => { alignItems: "center" }} > - +

{b.year} diff --git a/ts/features/bonus/cgn/__e2e__/cgn00.e2e.ts b/ts/features/bonus/cgn/__e2e__/cgn00.e2e.ts new file mode 100644 index 00000000000..96b609d2db7 --- /dev/null +++ b/ts/features/bonus/cgn/__e2e__/cgn00.e2e.ts @@ -0,0 +1,23 @@ +import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../../__e2e__/utils"; +import I18n from "../../../../i18n"; +import { ID_CGN_TYPE } from "../../common/utils"; +import { activateCGNBonusSuccess, deactivateCGNCardIfNeeded } from "./utils"; + +describe("CGN", () => { + beforeEach(async () => { + await deactivateCGNCardIfNeeded(); + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to start activation from bonus list, it should complete activation", async () => { + await element(by.text(I18n.t("global.navigator.wallet"))).tap(); + await element(by.id("walletAddNewPaymentMethodTestId")).tap(); + await element(by.id("bonusNameTestId")).tap(); + const cgnBonusItem = element(by.id(`AvailableBonusItem-${ID_CGN_TYPE}`)); + await waitFor(cgnBonusItem).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await cgnBonusItem.tap(); + await activateCGNBonusSuccess(); + }); +}); diff --git a/ts/features/bonus/cgn/__e2e__/cgn01.e2e.ts b/ts/features/bonus/cgn/__e2e__/cgn01.e2e.ts new file mode 100644 index 00000000000..0a1d0e19508 --- /dev/null +++ b/ts/features/bonus/cgn/__e2e__/cgn01.e2e.ts @@ -0,0 +1,23 @@ +import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../../__e2e__/utils"; +import I18n from "../../../../i18n"; +import { activateCGNBonusSuccess, deactivateCGNCardIfNeeded } from "./utils"; + +describe("CGN", () => { + beforeEach(async () => { + await deactivateCGNCardIfNeeded(); + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to start activation from card carousel, it should complete activation", async () => { + await element(by.text(I18n.t("global.navigator.wallet"))).tap(); + // TODO: This could be fail if we will add more e2e tests on the addition of a new payment method (just do a single swipe, not a scroll) + await waitFor(element(by.id("walletPaymentMethodsTestId"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await element(by.id("walletPaymentMethodsTestId")).swipe("up"); + await element(by.id("FeaturedCardCGNTestID")).tap(); + await activateCGNBonusSuccess(); + }); +}); diff --git a/ts/features/bonus/cgn/__e2e__/cgn02.e2e.ts b/ts/features/bonus/cgn/__e2e__/cgn02.e2e.ts new file mode 100644 index 00000000000..ed1a4374236 --- /dev/null +++ b/ts/features/bonus/cgn/__e2e__/cgn02.e2e.ts @@ -0,0 +1,35 @@ +import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../../__e2e__/utils"; +import I18n from "../../../../i18n"; +import { activateCGNBonusSuccess, deactivateCGNCardIfNeeded } from "./utils"; +const CGN_TITLE = "Carta Giovani Nazionale"; +const SERVICES_LIST = "services-list"; + +describe("CGN", () => { + beforeEach(async () => { + await deactivateCGNCardIfNeeded(); + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("When the user want to start activation from service detail, it should complete activation", async () => { + await element(by.text(I18n.t("global.navigator.services"))).tap(); + + await waitFor(element(by.id(SERVICES_LIST))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + await waitFor(element(by.id(CGN_TITLE))) + .toBeVisible() + .whileElement(by.id(SERVICES_LIST)) + .scroll(300, "down"); + + await element(by.id(CGN_TITLE)).tap(); + const startActivationCta = element(by.id("service-activate-bonus-button")); + await waitFor(startActivationCta) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await startActivationCta.tap(); + await activateCGNBonusSuccess(); + }); +}); diff --git a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts b/ts/features/bonus/cgn/__e2e__/utils.ts similarity index 51% rename from ts/features/bonus/cgn/__e2e__/cgn.e2e.ts rename to ts/features/bonus/cgn/__e2e__/utils.ts index a4f879c725f..dced57ab20b 100644 --- a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts +++ b/ts/features/bonus/cgn/__e2e__/utils.ts @@ -1,13 +1,8 @@ import fetch from "node-fetch"; import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; -import { ensureLoggedIn } from "../../../../__e2e__/utils"; import I18n from "../../../../i18n"; -import { ID_CGN_TYPE } from "../../common/utils"; -const CGN_TITLE = "Carta Giovani Nazionale"; -const SERVICES_LIST = "services-list"; - -const activateBonusSuccess = async () => { +export const activateCGNBonusSuccess = async () => { const startActivationCta = element(by.id("activate-bonus-button")); await waitFor(startActivationCta) .toBeVisible() @@ -28,7 +23,9 @@ const activateBonusSuccess = async () => { // The section has a loading spinner on top of // everything so we must wait for it to disappear - await waitFor(scrollView).toBeVisible().withTimeout(e2eWaitRenderTimeout); + await waitFor(scrollView) + .toBeVisible() + .withTimeout(2 * e2eWaitRenderTimeout); // make sure to scroll to bottom, otherwise in small devices the element will not be visible nor tappable await scrollView.scrollTo("bottom"); @@ -46,67 +43,7 @@ const activateBonusSuccess = async () => { await alertCTA.tap(); }; -describe("CGN", () => { - beforeEach(async () => { - await deactivateCGNCardIfNeeded(); - await device.launchApp({ newInstance: true }); - await ensureLoggedIn(); - }); - - describe("When the user want to start activation from bonus list", () => { - it("Should complete activation", async () => { - await element(by.text(I18n.t("global.navigator.wallet"))).tap(); - await element(by.id("walletAddNewPaymentMethodTestId")).tap(); - await element(by.id("bonusNameTestId")).tap(); - const cgnBonusItem = element(by.id(`AvailableBonusItem-${ID_CGN_TYPE}`)); - await waitFor(cgnBonusItem) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await cgnBonusItem.tap(); - await activateBonusSuccess(); - }); - }); - - describe("When the user want to start activation from card carousel", () => { - it("Should complete activation", async () => { - await element(by.text(I18n.t("global.navigator.wallet"))).tap(); - // TODO: This could be fail if we will add more e2e tests on the addition of a new payment method (just do a single swipe, not a scroll) - await waitFor(element(by.id("walletPaymentMethodsTestId"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await element(by.id("walletPaymentMethodsTestId")).swipe("up"); - await element(by.id("FeaturedCardCGNTestID")).tap(); - await activateBonusSuccess(); - }); - }); - - describe("When the user want to start activation from service detail", () => { - it("Should complete activation", async () => { - await element(by.text(I18n.t("global.navigator.services"))).tap(); - - await waitFor(element(by.id(SERVICES_LIST))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - await waitFor(element(by.id(CGN_TITLE))) - .toBeVisible() - .whileElement(by.id(SERVICES_LIST)) - .scroll(300, "down"); - - await element(by.id(CGN_TITLE)).tap(); - const startActivationCta = element( - by.id("service-activate-bonus-button") - ); - await waitFor(startActivationCta) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await startActivationCta.tap(); - await activateBonusSuccess(); - }); - }); -}); - -const deactivateCGNCardIfNeeded = async () => { +export const deactivateCGNCardIfNeeded = async () => { // This is needed since an E2E can fail at any moment. If it does so // after the card has been activated, all subsequent retries and tests // will fail, since the card is already activated. Be aware that this diff --git a/ts/features/bonus/cgn/analytics/index.ts b/ts/features/bonus/cgn/analytics/index.ts index 5a33f25f4f8..9de5bf76e5a 100644 --- a/ts/features/bonus/cgn/analytics/index.ts +++ b/ts/features/bonus/cgn/analytics/index.ts @@ -31,7 +31,7 @@ import { cgnCategories } from "../store/actions/categories"; const trackCgnAction = (mp: NonNullable) => // eslint-disable-next-line complexity - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(cgnActivationStart): case getType(cgnRequestActivation): @@ -86,7 +86,6 @@ const trackCgnAction = reason: action.payload }); } - return Promise.resolve(); }; export default trackCgnAction; diff --git a/ts/features/bonus/cgn/components/CgnCard.tsx b/ts/features/bonus/cgn/components/CgnCard.tsx new file mode 100644 index 00000000000..f877d99fa1d --- /dev/null +++ b/ts/features/bonus/cgn/components/CgnCard.tsx @@ -0,0 +1,107 @@ +import { H6, IOColors, LabelSmallAlt, Tag } from "@pagopa/io-app-design-system"; +import { format } from "date-fns"; +import * as React from "react"; +import { Image, StyleSheet, View } from "react-native"; +import cgnLogo from "../../../../../img/bonus/cgn/cgn_logo.png"; +import eycaLogo from "../../../../../img/bonus/cgn/eyca_logo.png"; +import CgnCardShape from "../../../../../img/features/cgn/cgn_card.svg"; +import I18n from "../../../../i18n"; + +export type CgnCardProps = { + expireDate?: Date; + withEycaLogo?: boolean; +}; + +export const CgnCard = ({ expireDate, withEycaLogo }: CgnCardProps) => { + const isExpired = expireDate === undefined; + + const eycaLogoComponent = ( + + + + ); + + const cngLogoComponent = ( + + + + ); + + const expiredTag = ( + + + + ); + + return ( + + + + + + +
{I18n.t("bonus.cgn.name")}
+ {isExpired && expiredTag} +
+ + {I18n.t("bonus.cgn.departmentName")} + + + {expireDate && + I18n.t("bonusCard.validUntil", { + endDate: format(expireDate, "MM/YY") + })} + +
+ {!isExpired && cngLogoComponent} + {withEycaLogo && eycaLogoComponent} +
+ ); +}; + +const styles = StyleSheet.create({ + container: { + aspectRatio: 16 / 10 + }, + card: { + position: "absolute", + transform: [{ rotateX: "180deg" }], + top: 0, + bottom: 0, + left: 0, + right: 0 + }, + content: { + flex: 1, + padding: 16, + paddingTop: 12, + justifyContent: "space-between" + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + height: 48 + }, + logoContainer: { + position: "absolute", + padding: 5, + backgroundColor: IOColors.white, + borderRadius: 8 + }, + logo: { + width: 30, + height: 30, + resizeMode: "contain" + } +}); diff --git a/ts/features/bonus/cgn/components/CgnServiceCTA.tsx b/ts/features/bonus/cgn/components/CgnServiceCTA.tsx index d4020fdc427..7ceaffee6c6 100644 --- a/ts/features/bonus/cgn/components/CgnServiceCTA.tsx +++ b/ts/features/bonus/cgn/components/CgnServiceCTA.tsx @@ -7,15 +7,15 @@ import { Label } from "../../../../components/core/typography/Label"; import ButtonDefaultOpacity from "../../../../components/ButtonDefaultOpacity"; import I18n from "../../../../i18n"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { servicePreferenceSelector } from "../../../../store/reducers/entities/services/servicePreference"; -import { isServicePreferenceResponseSuccess } from "../../../../types/services/ServicePreferenceResponse"; +import { servicePreferenceSelector } from "../../../services/store/reducers/servicePreference"; +import { isServicePreferenceResponseSuccess } from "../../../services/types/ServicePreferenceResponse"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { cgnActivationStart } from "../store/actions/activation"; import { cgnUnsubscribe } from "../store/actions/unsubscribe"; import { fold, isLoading } from "../../../../common/model/RemoteValue"; import { showToast } from "../../../../utils/showToast"; import { cgnUnsubscribeSelector } from "../store/reducers/unsubscribe"; -import { loadServicePreference } from "../../../../store/actions/services/servicePreference"; +import { loadServicePreference } from "../../../services/store/actions"; import ActivityIndicator from "../../../../components/ui/ActivityIndicator"; import { loadAvailableBonuses } from "../../common/store/actions/availableBonusesTypes"; diff --git a/ts/features/bonus/cgn/components/CgnWalletCard.tsx b/ts/features/bonus/cgn/components/CgnWalletCard.tsx new file mode 100644 index 00000000000..2af7ddb8784 --- /dev/null +++ b/ts/features/bonus/cgn/components/CgnWalletCard.tsx @@ -0,0 +1,40 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import * as React from "react"; +import { Pressable } from "react-native"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIOSelector } from "../../../../store/hooks"; +import { profileSelector } from "../../../../store/reducers/profile"; +import { withWalletCardBaseComponent } from "../../../newWallet/components/WalletCardBaseComponent"; +import CGN_ROUTES from "../navigation/routes"; +import { CgnCard, CgnCardProps } from "./CgnCard"; + +export type CgnWalletCardProps = CgnCardProps; + +const WrappedCgnCard = (props: CgnWalletCardProps) => { + const navigation = useIONavigation(); + const profilePot = useIOSelector(profileSelector); + + const isUnder31 = pipe( + pot.toOption(profilePot), + O.chainNullableK(({ date_of_birth }) => date_of_birth), + O.map(birthDate => new Date().getFullYear() - birthDate.getFullYear()), + O.map(years => years < 31), + O.getOrElse(() => false) + ); + + const handleCardPress = () => { + navigation.navigate(CGN_ROUTES.DETAILS.MAIN, { + screen: CGN_ROUTES.DETAILS.DETAILS + }); + }; + + return ( + + + + ); +}; + +export const CgnWalletCard = withWalletCardBaseComponent(WrappedCgnCard); diff --git a/ts/features/bonus/cgn/components/__test__/CgnCard.test.tsx b/ts/features/bonus/cgn/components/__test__/CgnCard.test.tsx new file mode 100644 index 00000000000..8c2e8e4826c --- /dev/null +++ b/ts/features/bonus/cgn/components/__test__/CgnCard.test.tsx @@ -0,0 +1,68 @@ +import { render } from "@testing-library/react-native"; +import { format } from "date-fns"; +import * as React from "react"; +import I18n from "../../../../../i18n"; +import { CgnCard } from "../CgnCard"; + +describe("CgnCard", () => { + it("should match the snapshot", () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + it("should correctly render a valid card with EYCA logo", () => { + const expireDate = new Date(2023, 11, 2); + const { queryByTestId, queryByText } = render( + + ); + expect(queryByTestId("cgnLogoTestID")).not.toBeNull(); + expect(queryByTestId("cgnExpiredTagTestID")).toBeNull(); + expect(queryByTestId("cgnEycaLogoTestID")).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.name"))).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.departmentName"))).not.toBeNull(); + expect( + queryByText( + I18n.t("bonusCard.validUntil", { + endDate: format(expireDate, "MM/YY") + }) + ) + ).not.toBeNull(); + }); + it("should correctly render a valid card without EYCA logo", () => { + const expireDate = new Date(2023, 11, 2); + const { queryByTestId, queryByText } = render( + + ); + expect(queryByTestId("cgnLogoTestID")).not.toBeNull(); + expect(queryByTestId("cgnExpiredTagTestID")).toBeNull(); + expect(queryByTestId("cgnEycaLogoTestID")).toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.name"))).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.departmentName"))).not.toBeNull(); + expect( + queryByText( + I18n.t("bonusCard.validUntil", { + endDate: format(expireDate, "MM/YY") + }) + ) + ).not.toBeNull(); + }); + it("should correctly render an expired card with EYCA logo", () => { + const { queryByTestId, queryByText } = render( + + ); + expect(queryByTestId("cgnLogoTestID")).toBeNull(); + expect(queryByTestId("cgnExpiredTagTestID")).not.toBeNull(); + expect(queryByTestId("cgnEycaLogoTestID")).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.name"))).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.departmentName"))).not.toBeNull(); + }); + it("should correctly render an expired card without EYCA logo", () => { + const { queryByTestId, queryByText } = render(); + expect(queryByTestId("cgnLogoTestID")).toBeNull(); + expect(queryByTestId("cgnExpiredTagTestID")).not.toBeNull(); + expect(queryByTestId("cgnEycaLogoTestID")).toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.name"))).not.toBeNull(); + expect(queryByText(I18n.t("bonus.cgn.departmentName"))).not.toBeNull(); + }); +}); diff --git a/ts/features/bonus/cgn/components/__test__/__snapshots__/CgnCard.test.tsx.snap b/ts/features/bonus/cgn/components/__test__/__snapshots__/CgnCard.test.tsx.snap new file mode 100644 index 00000000000..a0e44d51a06 --- /dev/null +++ b/ts/features/bonus/cgn/components/__test__/__snapshots__/CgnCard.test.tsx.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CgnCard should match the snapshot 1`] = ` + + + + + + + + Carta Giovani Nazionale + + + + Dipartimento per le politiche giovanili e il servizio civile universale + + + Valida fino al 12/23 + + + + + + + + + +`; diff --git a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx index ec0eebcd348..96a3d46f548 100644 --- a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx +++ b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { useEffect, useRef } from "react"; import { View, Alert } from "react-native"; -import { LabelLink } from "@pagopa/io-app-design-system"; +import { LabelLink, IOToast } from "@pagopa/io-app-design-system"; import { useIODispatch, useIOSelector } from "../../../../../store/hooks"; import { cgnUnsubscribeSelector } from "../../store/reducers/unsubscribe"; @@ -10,7 +10,6 @@ import { cgnUnsubscribe } from "../../store/actions/unsubscribe"; import { isError, isReady } from "../../../../../common/model/RemoteValue"; import { navigateBack } from "../../../../../store/actions/navigation"; import { cgnDetails } from "../../store/actions/details"; -import { IOToast } from "../../../../../components/Toast"; const CgnUnsubscribe = () => { const dispatch = useIODispatch(); diff --git a/ts/features/bonus/cgn/components/detail/eyca/EycaInformationComponent.tsx b/ts/features/bonus/cgn/components/detail/eyca/EycaInformationComponent.tsx index 46bd06d3d2e..c27021a1d94 100644 --- a/ts/features/bonus/cgn/components/detail/eyca/EycaInformationComponent.tsx +++ b/ts/features/bonus/cgn/components/detail/eyca/EycaInformationComponent.tsx @@ -1,12 +1,11 @@ import * as React from "react"; import { View } from "react-native"; -import { ButtonOutline, VSpacer } from "@pagopa/io-app-design-system"; -import Markdown from "../../../../../../components/ui/Markdown"; +import { ButtonOutline, VSpacer, IOToast } from "@pagopa/io-app-design-system"; +import LegacyMarkdown from "../../../../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../../../../i18n"; import { useIOBottomSheetAutoresizableModal } from "../../../../../../utils/hooks/bottomSheet"; import { openWebUrl } from "../../../../../../utils/url"; import { EYCA_WEBSITE_BASE_URL } from "../../../utils/constants"; -import { IOToast } from "../../../../../../components/Toast"; /** * this component shows information about EYCA card. It is included within a bottom sheet @@ -18,9 +17,12 @@ const EycaInformationComponent: React.FunctionComponent = () => { - setMarkdownloaded(true)}> + setMarkdownloaded(true)} + > {I18n.t("bonus.cgn.detail.status.eycaDescription")} - + {isMarkdownloaded && ( void; diff --git a/ts/features/bonus/cgn/components/detail/eyca/EycaStatusDetailsComponent.tsx b/ts/features/bonus/cgn/components/detail/eyca/EycaStatusDetailsComponent.tsx index 7eb20a9b9e1..813ee5689f4 100644 --- a/ts/features/bonus/cgn/components/detail/eyca/EycaStatusDetailsComponent.tsx +++ b/ts/features/bonus/cgn/components/detail/eyca/EycaStatusDetailsComponent.tsx @@ -6,6 +6,7 @@ import { HSpacer, VSpacer, IOSpacingScale, + IOToast, Badge, ButtonOutline } from "@pagopa/io-app-design-system"; @@ -23,7 +24,6 @@ import { clipboardSetStringWithFeedback } from "../../../../../../utils/clipboar import TouchableDefaultOpacity from "../../../../../../components/TouchableDefaultOpacity"; import { openWebUrl } from "../../../../../../utils/url"; import { EYCA_WEBSITE_DISCOUNTS_PAGE_URL } from "../../../utils/constants"; -import { IOToast } from "../../../../../../components/Toast"; type Props = { eycaCard: EycaCardActivated | EycaCardExpired | EycaCardRevoked; diff --git a/ts/features/bonus/cgn/components/merchants/CgnDiscountDetail.tsx b/ts/features/bonus/cgn/components/merchants/CgnDiscountDetail.tsx index 84ce1f6c391..3d3f8e40800 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnDiscountDetail.tsx +++ b/ts/features/bonus/cgn/components/merchants/CgnDiscountDetail.tsx @@ -8,6 +8,7 @@ import { HSpacer, VSpacer, Icon, + IOToast, IOIconSizeScale, ButtonOutline } from "@pagopa/io-app-design-system"; @@ -25,7 +26,6 @@ import { mixpanelTrack } from "../../../../../mixpanel"; import { useIOSelector } from "../../../../../store/hooks"; import { profileSelector } from "../../../../../store/reducers/profile"; import { localeDateFormat } from "../../../../../utils/locale"; -import { IOToast } from "../../../../../components/Toast"; import { openWebUrl } from "../../../../../utils/url"; import { getCgnUserAgeRange } from "../../utils/dates"; import { getCategorySpecs } from "../../utils/filters"; diff --git a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx index 3308dbca6db..be91b904616 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx +++ b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx @@ -43,7 +43,7 @@ const CgnMerchantsListView: React.FunctionComponent = (props: Props) => { keyExtractor={c => c.id} keyboardShouldPersistTaps={"handled"} ListFooterComponent={ - props.merchantList.length > 0 && + props.merchantList.length > 0 ? : null } /> diff --git a/ts/features/bonus/cgn/components/merchants/__test__/__snapshots__/CgnDiscountDetail.test.tsx.snap b/ts/features/bonus/cgn/components/merchants/__test__/__snapshots__/CgnDiscountDetail.test.tsx.snap index 900c53e914d..33405951ef8 100644 --- a/ts/features/bonus/cgn/components/merchants/__test__/__snapshots__/CgnDiscountDetail.test.tsx.snap +++ b/ts/features/bonus/cgn/components/merchants/__test__/__snapshots__/CgnDiscountDetail.test.tsx.snap @@ -128,7 +128,6 @@ exports[`when rendering on match snapshot for OTP discount 1`] = ` align="xMidYMid" bbHeight={24} bbWidth={24} - color={4278219750} focusable={false} height={24} importantForAccessibility="no-hide-descendants" @@ -151,19 +150,26 @@ exports[`when rendering on match snapshot for OTP discount 1`] = ` }, ] } - tintColor={4278219750} + tintColor="#0073E6" vbHeight={24} vbWidth={24} width={24} > - + - + - + = { [CGN_ROUTES.DETAILS.MAIN]: { path: "cgn-details", screens: { @@ -48,8 +51,7 @@ const ActivationStack = createStackNavigator(); export const CgnActivationNavigator = () => ( (); export const CgnDetailsNavigator = () => ( (); export const CgnEYCAActivationNavigator = () => ( ["getCgnStatus"], @@ -30,7 +32,23 @@ export function* cgnGetInformationSaga( E.isRight(cgnInformationResult) && cgnInformationResult.right.status === 200 ) { - yield* put(cgnDetails.success(cgnInformationResult.right.value)); + const cgnInfo = cgnInformationResult.right.value; + const expireDate = + cgnInfo.status === StatusEnum.ACTIVATED + ? cgnInfo.expiration_date + : undefined; + + yield* put( + walletAddCards([ + { + type: "cgn", + category: "cgn", + key: "cgn_card", + expireDate + } + ]) + ); + yield* put(cgnDetails.success(cgnInfo)); } else { yield* put( cgnDetails.failure({ diff --git a/ts/features/bonus/cgn/saga/orchestration/activation/activationSaga.ts b/ts/features/bonus/cgn/saga/orchestration/activation/activationSaga.ts index 001e9abb69b..4c76242618e 100644 --- a/ts/features/bonus/cgn/saga/orchestration/activation/activationSaga.ts +++ b/ts/features/bonus/cgn/saga/orchestration/activation/activationSaga.ts @@ -1,6 +1,8 @@ +import { CommonActions } from "@react-navigation/native"; import { SagaIterator } from "redux-saga"; import { call } from "typed-redux-saga/macro"; import NavigationService from "../../../../../../navigation/NavigationService"; +import ROUTES from "../../../../../../navigation/routes"; import { executeWorkUnit, withResetNavigationStack @@ -8,6 +10,7 @@ import { import { navigateBack } from "../../../../../../store/actions/navigation"; import { SagaCallReturnType } from "../../../../../../types/utils"; import { MESSAGES_ROUTES } from "../../../../../messages/navigation/routes"; +import { WalletRoutes } from "../../../../../newWallet/navigation/routes"; import { BONUS_ROUTES } from "../../../../common/navigation/navigator"; import { navigateToCgnActivationInformationTos, @@ -45,12 +48,38 @@ export function* handleCgnStartActivationSaga(): SagaIterator { ); if (initialScreen?.name === CGN_ROUTES.ACTIVATION.CTA_START_CGN) { - yield* call(NavigationService.navigate, MESSAGES_ROUTES.MESSAGES_HOME); + yield* call(NavigationService.navigate, ROUTES.MAIN, { + screen: MESSAGES_ROUTES.MESSAGES_HOME + }); } if (result === "completed") { if (initialScreen?.name === BONUS_ROUTES.BONUS_AVAILABLE_LIST) { yield* call(navigateBack); + yield* call(navigateToCgnDetails); + } else if (initialScreen?.name === WalletRoutes.WALLET_CARD_ONBOARDING) { + yield* call( + NavigationService.dispatchNavigationAction, + CommonActions.reset({ + index: 0, + routes: [ + { + name: ROUTES.MAIN, + params: { + screen: ROUTES.WALLET_HOME, + params: { newMethodAdded: true } + } + }, + { + name: CGN_ROUTES.DETAILS.MAIN, + params: { + screen: CGN_ROUTES.DETAILS.DETAILS + } + } + ] + }) + ); + } else { + yield* call(navigateToCgnDetails); } - yield* call(navigateToCgnDetails); } } diff --git a/ts/features/bonus/cgn/screens/activation/CgnActivationCompletedScreen.tsx b/ts/features/bonus/cgn/screens/activation/CgnActivationCompletedScreen.tsx index 33b2b6d96c8..c2f24ea5eac 100644 --- a/ts/features/bonus/cgn/screens/activation/CgnActivationCompletedScreen.tsx +++ b/ts/features/bonus/cgn/screens/activation/CgnActivationCompletedScreen.tsx @@ -1,57 +1,32 @@ import * as React from "react"; -import { connect } from "react-redux"; -import { SafeAreaView, View } from "react-native"; -import { FooterWithButtons } from "@pagopa/io-app-design-system"; - -import { GlobalState } from "../../../../../store/reducers/types"; -import { Dispatch } from "../../../../../store/actions/types"; -import { InfoScreenComponent } from "../../../../../components/infoScreen/InfoScreenComponent"; -import { IOStyles } from "../../../../../components/core/variables/IOStyles"; import { cgnActivationComplete } from "../../store/actions/activation"; import I18n from "../../../../../i18n"; -import paymentCompleted from "../../../../../../img/pictograms/payment-completed.png"; -import { renderInfoRasterImage } from "../../../../../components/infoScreen/imageRendering"; - -type Props = ReturnType & - ReturnType; +import { useIODispatch } from "../../../../../store/hooks"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; /** * Screen which is displayed when a user requested a CGN activation * and it has been correctly activated */ -const CgnActivationCompletedScreen = (props: Props): React.ReactElement => ( - - { + const dispatch = useIODispatch(); + const onConfirm = React.useCallback(() => { + dispatch(cgnActivationComplete()); + }, [dispatch]); + + return ( + - - - - -); - -const mapStateToProps = (_: GlobalState) => ({}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onConfirm: () => { - dispatch(cgnActivationComplete()); - } -}); + ); +}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CgnActivationCompletedScreen); +export default CgnActivationCompletedScreen; diff --git a/ts/features/bonus/cgn/screens/activation/CgnActivationIneligibleScreen.tsx b/ts/features/bonus/cgn/screens/activation/CgnActivationIneligibleScreen.tsx index e8a90eedf46..baf5ee88bba 100644 --- a/ts/features/bonus/cgn/screens/activation/CgnActivationIneligibleScreen.tsx +++ b/ts/features/bonus/cgn/screens/activation/CgnActivationIneligibleScreen.tsx @@ -1,53 +1,31 @@ import * as React from "react"; -import { connect } from "react-redux"; -import { SafeAreaView, View } from "react-native"; -import { FooterWithButtons } from "@pagopa/io-app-design-system"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { Dispatch } from "../../../../../store/actions/types"; -import { InfoScreenComponent } from "../../../../../components/infoScreen/InfoScreenComponent"; -import { renderInfoRasterImage } from "../../../../../components/infoScreen/imageRendering"; -import { IOStyles } from "../../../../../components/core/variables/IOStyles"; import { cgnActivationCancel } from "../../store/actions/activation"; -import image from "../../../../../../img/servicesStatus/error-detail-icon.png"; import I18n from "../../../../../i18n"; - -type Props = ReturnType & - ReturnType; +import { useIODispatch } from "../../../../../store/hooks"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; /** * Screen which is displayed when a user requested a CGN activation * but is not eligible for its activation */ -const CgnActivationIneligibleScreen = (props: Props): React.ReactElement => ( - - { + const dispatch = useIODispatch(); + const onExit = React.useCallback( + () => dispatch(cgnActivationCancel()), + [dispatch] + ); + return ( + - - - - -); - -const mapStateToProps = (_: GlobalState) => ({}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onCancel: () => dispatch(cgnActivationCancel()) -}); + ); +}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CgnActivationIneligibleScreen); +export default CgnActivationIneligibleScreen; diff --git a/ts/features/bonus/cgn/screens/activation/CgnActivationPendingScreen.tsx b/ts/features/bonus/cgn/screens/activation/CgnActivationPendingScreen.tsx index e0a7f2278e0..bbae18593f9 100644 --- a/ts/features/bonus/cgn/screens/activation/CgnActivationPendingScreen.tsx +++ b/ts/features/bonus/cgn/screens/activation/CgnActivationPendingScreen.tsx @@ -1,54 +1,32 @@ import * as React from "react"; -import { connect } from "react-redux"; -import { SafeAreaView, View } from "react-native"; -import { FooterWithButtons } from "@pagopa/io-app-design-system"; - -import { GlobalState } from "../../../../../store/reducers/types"; -import { Dispatch } from "../../../../../store/actions/types"; -import { IOStyles } from "../../../../../components/core/variables/IOStyles"; -import { InfoScreenComponent } from "../../../../../components/infoScreen/InfoScreenComponent"; -import { renderInfoRasterImage } from "../../../../../components/infoScreen/imageRendering"; -import image from "../../../../../../img/messages/empty-message-list-icon.png"; import I18n from "../../../../../i18n"; import { cgnActivationCancel } from "../../store/actions/activation"; - -type Props = ReturnType & - ReturnType; +import { useIODispatch } from "../../../../../store/hooks"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; /** * Screen which is displayed when a user requested a CGN activation * and the server has already another request pending for the user */ -const CgnActivationPendingScreen = (props: Props): React.ReactElement => ( - - { + const dispatch = useIODispatch(); + const onExit = React.useCallback( + () => dispatch(cgnActivationCancel()), + [dispatch] + ); + + return ( + - - - - -); - -const mapStateToProps = (_: GlobalState) => ({}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onExit: () => dispatch(cgnActivationCancel()) -}); + ); +}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CgnActivationPendingScreen); +export default CgnActivationPendingScreen; diff --git a/ts/features/bonus/cgn/screens/activation/CgnActivationTimeoutScreen.tsx b/ts/features/bonus/cgn/screens/activation/CgnActivationTimeoutScreen.tsx index 8dafd6abd3d..d472c6135ef 100644 --- a/ts/features/bonus/cgn/screens/activation/CgnActivationTimeoutScreen.tsx +++ b/ts/features/bonus/cgn/screens/activation/CgnActivationTimeoutScreen.tsx @@ -1,34 +1,33 @@ import * as React from "react"; -import { connect } from "react-redux"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { Dispatch } from "../../../../../store/actions/types"; -import { BaseTimeoutScreen } from "../../../common/BaseTimeoutScreen"; import { cgnActivationCancel } from "../../store/actions/activation"; import I18n from "../../../../../i18n"; - -type Props = ReturnType & - ReturnType; +import { useIODispatch } from "../../../../../store/hooks"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; /** * Screen which is displayed when a user requested a CGN activation * and it took too long to get an answer from the server * (the user will be notified when the activation is completed by a message) */ -const CgnActivationTimeoutScreen = (props: Props): React.ReactElement => ( - -); - -const mapStateToProps = (_: GlobalState) => ({}); +const CgnActivationTimeoutScreen = () => { + const dispatch = useIODispatch(); + const onExit = React.useCallback( + () => dispatch(cgnActivationCancel()), + [dispatch] + ); -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onExit: () => dispatch(cgnActivationCancel()) -}); + return ( + + ); +}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CgnActivationTimeoutScreen); +export default CgnActivationTimeoutScreen; diff --git a/ts/features/bonus/cgn/screens/activation/CgnAlreadyActiveScreen.tsx b/ts/features/bonus/cgn/screens/activation/CgnAlreadyActiveScreen.tsx index dd86514c0e3..e21b833213f 100644 --- a/ts/features/bonus/cgn/screens/activation/CgnAlreadyActiveScreen.tsx +++ b/ts/features/bonus/cgn/screens/activation/CgnAlreadyActiveScreen.tsx @@ -1,58 +1,39 @@ import * as React from "react"; -import { connect } from "react-redux"; -import { SafeAreaView, View } from "react-native"; -import { FooterWithButtons } from "@pagopa/io-app-design-system"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { Dispatch } from "../../../../../store/actions/types"; -import { InfoScreenComponent } from "../../../../../components/infoScreen/InfoScreenComponent"; -import { renderInfoRasterImage } from "../../../../../components/infoScreen/imageRendering"; -import { IOStyles } from "../../../../../components/core/variables/IOStyles"; import { cgnActivationCancel } from "../../store/actions/activation"; -import image from "../../../../../../img/messages/empty-due-date-list-icon.png"; import I18n from "../../../../../i18n"; -import { navigateToCgnDetails } from "../../navigation/actions"; - -type Props = ReturnType & - ReturnType; +import { useIODispatch } from "../../../../../store/hooks"; +import { useIONavigation } from "../../../../../navigation/params/AppParamsList"; +import CGN_ROUTES from "../../navigation/routes"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; /** * Screen which is displayed when a user requested a CGN activation * but it is yet active */ -const CgnAlreadyActiveScreen = (props: Props): React.ReactElement => ( - - - - - - -); - -const mapStateToProps = (_: GlobalState) => ({}); +const CgnAlreadyActiveScreen = () => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); -const mapDispatchToProps = (dispatch: Dispatch) => ({ - navigateToDetail: () => { + const navigateToDetail = React.useCallback(() => { dispatch(cgnActivationCancel()); - navigateToCgnDetails(); - } -}); + navigation.navigate(CGN_ROUTES.DETAILS.MAIN, { + screen: CGN_ROUTES.DETAILS.DETAILS + }); + }, [dispatch, navigation]); + + return ( + + ); +}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CgnAlreadyActiveScreen); +export default CgnAlreadyActiveScreen; diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx index 0e2170aa72e..f3add25c6cd 100644 --- a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx @@ -9,6 +9,7 @@ import { getGradientColorValues, VSpacer, Badge, + IOToast, Icon } from "@pagopa/io-app-design-system"; import { ProductCategoryWithNewDiscountsCount } from "../../../../../../definitions/cgn/merchants/ProductCategoryWithNewDiscountsCount"; @@ -26,7 +27,6 @@ import CGN_ROUTES from "../../navigation/routes"; import { cgnCategories } from "../../store/actions/categories"; import { cgnCategoriesListSelector } from "../../store/reducers/categories"; import { getCategorySpecs } from "../../utils/filters"; -import { IOToast } from "../../../../../components/Toast"; const CgnMerchantsCategoriesSelectionScreen = () => { const isFirstRender = useRef(true); diff --git a/ts/features/bonus/common/BaseTimeoutScreen.tsx b/ts/features/bonus/common/BaseTimeoutScreen.tsx deleted file mode 100644 index c6cdc7c0fcc..00000000000 --- a/ts/features/bonus/common/BaseTimeoutScreen.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"; -import { SafeAreaView } from "react-native"; -import image from "../../../../img/wallet/errors/payment-expired-icon.png"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { renderInfoRasterImage } from "../../../components/infoScreen/imageRendering"; -import { InfoScreenComponent } from "../../../components/infoScreen/InfoScreenComponent"; -import I18n from "../../../i18n"; -import { cancelButtonProps } from "../../../components/buttons/ButtonConfigurations"; -import { FooterStackButton } from "../../../components/buttons/FooterStackButtons"; - -type Props = { - title: string; - body: string | React.ReactNode; - onExit: () => void; -}; - -/** - * This screen informs the user that the request takes longer than necessary to be completed - * and will receive a notification with the outcome of the operation. - * @param props - * @constructor - */ - -export const BaseTimeoutScreen: React.FunctionComponent = props => { - const confirmText = I18n.t("global.buttons.exit"); - return ( - - - - - ); -}; diff --git a/ts/features/bonus/common/components/BonusInformationComponent.tsx b/ts/features/bonus/common/components/BonusInformationComponent.tsx index f5f9f6a0788..9e8034b9fbc 100644 --- a/ts/features/bonus/common/components/BonusInformationComponent.tsx +++ b/ts/features/bonus/common/components/BonusInformationComponent.tsx @@ -30,7 +30,7 @@ import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponen import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import { EdgeBorderComponent } from "../../../../components/screens/EdgeBorderComponent"; import { LightModalContextInterface } from "../../../../components/ui/LightModal"; -import Markdown from "../../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../../i18n"; import customVariables from "../../../../theme/variables"; import { useScreenReaderEnabled } from "../../../../utils/accessibility"; @@ -129,7 +129,7 @@ const getTosFooter = ( - @@ -138,7 +138,7 @@ const getTosFooter = ( regulationLink: rU.url, tosUrl: bT })} - + ) ) @@ -259,13 +259,13 @@ const BonusInformationComponent: React.FunctionComponent = props => { - {bonusTypeLocalizedContent.content} - + {isMarkdownLoaded && renderUrls()} {getTosFooter( diff --git a/ts/features/bonus/common/components/DeclarationEntry.tsx b/ts/features/bonus/common/components/DeclarationEntry.tsx deleted file mode 100644 index fea7b09b00d..00000000000 --- a/ts/features/bonus/common/components/DeclarationEntry.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import { View, StyleSheet, TouchableWithoutFeedback } from "react-native"; -import { HSpacer, VSpacer } from "@pagopa/io-app-design-system"; -import { Body } from "../../../../components/core/typography/Body"; -import { XOR } from "../../../../types/utils"; -import { RawCheckBox } from "../../../../components/core/selection/checkbox/RawCheckBox"; - -const styles = StyleSheet.create({ - main: { flex: 1, flexDirection: "row", flexWrap: "nowrap" }, - shrink: { flexShrink: 1 } -}); - -type Props = { - // in order to accepts composite text with bold - text: XOR; - onValueChange: (value: boolean) => void; -}; - -/** - * Choose between a string or a node - * @param text - */ -const pickText = (text: XOR) => - typeof text === "string" ? {text} : text; - -/** - * A declaration entry (checkbox + text) that the user have to accept in order to continue - * @constructor - */ -export const DeclarationEntry: React.FunctionComponent = props => { - const [isChecked, setIsChecked] = React.useState(false); - const handleOnPress = () => { - const newValue = !isChecked; - setIsChecked(newValue); - props.onValueChange(newValue); - }; - - return ( - - - - - - - {pickText(props.text)} - - - - - - ); -}; diff --git a/ts/features/bonus/common/navigation/navigator.tsx b/ts/features/bonus/common/navigation/navigator.tsx index 619409eae9f..8fbfa158274 100644 --- a/ts/features/bonus/common/navigation/navigator.tsx +++ b/ts/features/bonus/common/navigation/navigator.tsx @@ -19,8 +19,7 @@ const BonusStack = createStackNavigator(); export const BonusNavigator = () => ( & ReturnType; @@ -271,7 +271,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(showServiceDetails(service)) }); +const AvailableBonusScreenFC: React.FunctionComponent = ( + props: Props +) => ( + + + +); + export default connect( mapStateToProps, mapDispatchToProps -)(withLoadingSpinner(AvailableBonusScreen)); +)(AvailableBonusScreenFC); diff --git a/ts/features/bonus/common/store/selectors/index.ts b/ts/features/bonus/common/store/selectors/index.ts index 9967af718e6..a469485e2ed 100644 --- a/ts/features/bonus/common/store/selectors/index.ts +++ b/ts/features/bonus/common/store/selectors/index.ts @@ -8,7 +8,7 @@ import { GlobalState } from "../../../../../store/reducers/types"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { BonusVisibilityEnum } from "../../../../../../definitions/content/BonusVisibility"; -import { servicesByIdSelector } from "../../../../../store/reducers/entities/services/servicesById"; +import { servicesByIdSelector } from "../../../../services/store/reducers/servicesById"; import { mapBonusIdFeatureFlag } from "../../utils"; import { AvailableBonusTypesState } from "../reducers/availableBonusesTypes"; diff --git a/ts/features/common/store/reducers/index.ts b/ts/features/common/store/reducers/index.ts index 06d34f28b66..b0ed82336b6 100644 --- a/ts/features/common/store/reducers/index.ts +++ b/ts/features/common/store/reducers/index.ts @@ -28,13 +28,14 @@ import { CieLoginState } from "../../../cieLogin/store/reducers"; -import walletV3Reducer, { - WalletState as WalletV3State -} from "../../../walletV3/common/store/reducers"; +import paymentsReducer, { + PaymentsState +} from "../../../payments/common/store/reducers"; import { fastLoginReducer, FastLoginState } from "../../../fastLogin/store/reducers"; +import walletReducer, { WalletState } from "../../../newWallet/store/reducers"; type LoginFeaturesState = { testLogin: TestLoginState; @@ -50,7 +51,8 @@ export type FeaturesState = { idPay: IDPayState; whatsNew: WhatsNewState & PersistPartial; loginFeatures: LoginFeaturesState; - wallet: WalletV3State; + payments: PaymentsState; + wallet: WalletState; }; export type PersistedFeaturesState = FeaturesState & PersistPartial; @@ -60,14 +62,15 @@ const rootReducer = combineReducers({ pn: pnReducer, fci: fciReducer, idPay: idPayReducer, - wallet: walletV3Reducer, + payments: paymentsReducer, whatsNew: whatsNewPersistor, loginFeatures: combineReducers({ testLogin: testLoginReducer, nativeLogin: nativeLoginReducer, fastLogin: fastLoginReducer, cieLogin: cieLoginReducer - }) + }), + wallet: walletReducer }); const CURRENT_REDUX_FEATURES_STORE_VERSION = 1; diff --git a/ts/features/design-system/DesignSystem.tsx b/ts/features/design-system/DesignSystem.tsx index e70966a061c..1fbbba666f7 100644 --- a/ts/features/design-system/DesignSystem.tsx +++ b/ts/features/design-system/DesignSystem.tsx @@ -1,26 +1,17 @@ -import { SectionList, StatusBar, View, useColorScheme } from "react-native"; -import * as React from "react"; import { - useIOTheme, Divider, - VSpacer, + IOStyles, + IOVisualCostants, ListItemNav, - IOVisualCostants + VSpacer, + useIOTheme } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { - AppParamsList, - IOStackNavigationRouteProps -} from "../../navigation/params/AppParamsList"; +import * as React from "react"; +import { SectionList, StatusBar, View, useColorScheme } from "react-native"; import { H1 } from "../../components/core/typography/H1"; import { LabelSmall } from "../../components/core/typography/LabelSmall"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import DESIGN_SYSTEM_ROUTES from "./navigation/routes"; -import { DesignSystemParamsList } from "./navigation/params"; - -type Props = IOStackNavigationRouteProps< - DesignSystemParamsList, - "DESIGN_SYSTEM_MAIN" ->; type SingleSectionProps = { title: string; @@ -78,9 +69,10 @@ const DESIGN_SYSTEM_SECTION_DATA = [ } ]; -export const DesignSystem = (props: Props) => { +export const DesignSystem = () => { const theme = useIOTheme(); const colorScheme = useColorScheme(); + const navigation = useIONavigation(); const renderDSNavItem = ({ item: { title, route } @@ -90,7 +82,7 @@ export const DesignSystem = (props: Props) => { props.navigation.navigate(route as keyof AppParamsList)} + onPress={() => navigation.navigate(route as any)} /> ); @@ -115,6 +107,7 @@ export const DesignSystem = (props: Props) => { <> `${item.route}-${index}`} diff --git a/ts/features/design-system/components/DSIconViewerBox.tsx b/ts/features/design-system/components/DSIconViewerBox.tsx index b666dbf6a03..dc103082821 100644 --- a/ts/features/design-system/components/DSIconViewerBox.tsx +++ b/ts/features/design-system/components/DSIconViewerBox.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { View, StyleSheet, Text } from "react-native"; -import { IOColors, IOThemeContext } from "@pagopa/io-app-design-system"; +import { IOColors, useIOTheme } from "@pagopa/io-app-design-system"; export const iconItemGutter = 8; @@ -91,40 +91,39 @@ export const DSIconViewerBox = ({ image, size, withDot = false -}: DSIconViewerBoxProps) => ( - - {theme => ( +}: DSIconViewerBoxProps) => { + const theme = useIOTheme(); + return ( + - - {withDot && } - {image} - - - {name && ( - - {name} - - )} - + {withDot && } + {image} - )} - -); + + {name && ( + + {name} + + )} + + + ); +}; diff --git a/ts/features/design-system/components/DSSpacerViewerBox.tsx b/ts/features/design-system/components/DSSpacerViewerBox.tsx index 5aa059b52fc..33127fab7cc 100644 --- a/ts/features/design-system/components/DSSpacerViewerBox.tsx +++ b/ts/features/design-system/components/DSSpacerViewerBox.tsx @@ -2,11 +2,11 @@ import * as React from "react"; import { View, Text } from "react-native"; import { IOColors, - IOThemeContext, VSpacer, HSpacer, IOSpacer, - SpacerOrientation + SpacerOrientation, + useIOTheme } from "@pagopa/io-app-design-system"; type DSSpacerViewerBoxProps = { @@ -18,60 +18,58 @@ type DSSpacerLabelProps = { value: IOSpacer; }; -const DSSpacerLabel = ({ value }: DSSpacerLabelProps) => ( - - {theme => ( - - {value} - - )} - -); +const DSSpacerLabel = ({ value }: DSSpacerLabelProps) => { + const theme = useIOTheme(); + return ( + + {value} + + ); +}; export const DSSpacerViewerBox = ({ size, orientation = "vertical" -}: DSSpacerViewerBoxProps) => ( - - {theme => ( - <> - {orientation === "vertical" ? ( - - - - - {size && ( - - - - )} +}: DSSpacerViewerBoxProps) => { + const theme = useIOTheme(); + return ( + <> + {orientation === "vertical" ? ( + + + - ) : ( - - - + {size && ( + + - {size && ( - - - - )} + )} + + ) : ( + + + - )} - - )} - -); + {size && ( + + + + )} + + )} + + ); +}; diff --git a/ts/features/design-system/components/DesignSystemScreen.tsx b/ts/features/design-system/components/DesignSystemScreen.tsx index edf7b21d0e3..154c1d26f4c 100644 --- a/ts/features/design-system/components/DesignSystemScreen.tsx +++ b/ts/features/design-system/components/DesignSystemScreen.tsx @@ -23,6 +23,7 @@ export const DesignSystemScreen = ({ children, noMargin = false }: Props) => { <> { + const theme = useIOTheme(); const handlePressDismiss = () => { dismissStaticBottomSheet(); dismissStaticBottomSheetWithFooter(); @@ -186,85 +187,79 @@ export const DSBottomSheet = () => { ); return ( - - {theme => ( - -

- Available bottom sheets -

- - - - - - -

- Legacy -

+ +

+ Available bottom sheets +

+ + + + + + +

+ Legacy +

- - - - {staticBottomSheet} - {staticBottomSheetWithFooter} - {autoResizableBottomSheet} - {autoResizableBottomSheetWithFooter} - {veryLongAutoResizableBottomSheetWithFooter} - {veryLongAutoResizableBottomSheetWithFooterFullScreen} - {legacyBottomSheet} - {legacyBottomSheetWithFooter} -
- )} -
+ + + + {staticBottomSheet} + {staticBottomSheetWithFooter} + {autoResizableBottomSheet} + {autoResizableBottomSheetWithFooter} + {veryLongAutoResizableBottomSheetWithFooter} + {veryLongAutoResizableBottomSheetWithFooterFullScreen} + {legacyBottomSheet} + {legacyBottomSheetWithFooter} + ); }; diff --git a/ts/features/design-system/core/DSButtons.tsx b/ts/features/design-system/core/DSButtons.tsx index c5ed0e27ad6..70a2d2b83f5 100644 --- a/ts/features/design-system/core/DSButtons.tsx +++ b/ts/features/design-system/core/DSButtons.tsx @@ -1,29 +1,29 @@ -import { View, StyleSheet, Alert } from "react-native"; -import * as React from "react"; -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; import { + BlockButtons, ButtonLink, ButtonOutline, ButtonSolid, + HSpacer, + IOColors, IconButton, - IconButtonSolid, IconButtonContained, - IOColors, - HSpacer, + IconButtonSolid, + ListItemSwitch, VSpacer, useIOExperimentalDesign, - useIOTheme, - BlockButtons, - ListItemSwitch + useIOTheme } from "@pagopa/io-app-design-system"; +import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; +import * as React from "react"; import { useState } from "react"; -import { H2 } from "../../../components/core/typography/H2"; +import { Alert, StyleSheet, View } from "react-native"; +import { PaymentNoticeNumber } from "../../../../definitions/backend/PaymentNoticeNumber"; import CopyButtonComponent from "../../../components/CopyButtonComponent"; +import { H2 } from "../../../components/core/typography/H2"; +import { IOStyles } from "../../../components/core/variables/IOStyles"; import PaymentButton from "../../messages/components/MessageDetail/PaymentButton"; -import { PaymentNoticeNumber } from "../../../../definitions/backend/PaymentNoticeNumber"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; const styles = StyleSheet.create({ primaryBlockLegacy: { @@ -78,7 +78,7 @@ export const DSButtons = () => { > ButtonLink

- {renderButtonLink()} + {renderButtonLink(isExperimental)} @@ -628,7 +628,7 @@ const renderButtonOutline = (isExperimental: boolean) => ( ); -const renderButtonLink = () => ( +const renderButtonLink = (isExperimental: boolean) => ( <> ( /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/ts/features/design-system/core/DSCards.tsx b/ts/features/design-system/core/DSCards.tsx index 0b2cfd7a0b1..1c94441f2d3 100644 --- a/ts/features/design-system/core/DSCards.tsx +++ b/ts/features/design-system/core/DSCards.tsx @@ -1,14 +1,18 @@ -import * as React from "react"; -import { Alert, StyleSheet, View } from "react-native"; import { HSpacer, VSpacer } from "@pagopa/io-app-design-system"; -import { PaymentCardBig } from "../../../components/ui/cards/payment/PaymentCardBig"; -import { PaymentCardSmall } from "../../../components/ui/cards/payment/PaymentCardSmall"; +import * as React from "react"; +import { Alert, ScrollView, StyleSheet, View } from "react-native"; +import { CgnCard } from "../../bonus/cgn/components/CgnCard"; +import { IdPayCard } from "../../idpay/wallet/components/IdPayCard"; +import { PaymentCard } from "../../payments/common/components/PaymentCard"; +import { PaymentCardBig } from "../../payments/common/components/PaymentCardBig"; +import { PaymentCardSmall } from "../../payments/common/components/PaymentCardSmall"; import { PaymentCardsCarousel, PaymentCardsCarouselProps -} from "../../../components/ui/cards/payment/PaymentCardsCarousel"; +} from "../../payments/common/components/PaymentCardsCarousel"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; +import { DesignSystemSection } from "../components/DesignSystemSection"; const styles = StyleSheet.create({ content: { @@ -68,87 +72,132 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = { // https://www.comuniecitta.it/elenco-banche-per-codice-abi export const DSCards = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + ); diff --git a/ts/features/design-system/core/DSHapticFeedback.tsx b/ts/features/design-system/core/DSHapticFeedback.tsx index 7e45720299f..b7fe2194404 100644 --- a/ts/features/design-system/core/DSHapticFeedback.tsx +++ b/ts/features/design-system/core/DSHapticFeedback.tsx @@ -1,88 +1,79 @@ import * as React from "react"; import ReactNativeHapticFeedback from "react-native-haptic-feedback"; -import { - ButtonSolid, - IOThemeContext, - VSpacer -} from "@pagopa/io-app-design-system"; +import { ButtonSolid, VSpacer, useIOTheme } from "@pagopa/io-app-design-system"; import { H2 } from "../../../components/core/typography/H2"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; -export const DSHapticFeedback = () => ( - - {theme => ( - -

Feedback

- - ReactNativeHapticFeedback.trigger("impactLight")} - accessibilityLabel="impactLight" - accessibilityHint="impactLight" - /> - - ReactNativeHapticFeedback.trigger("impactMedium")} - accessibilityLabel="impactMedium" - accessibilityHint="impactMedium" - /> - - ReactNativeHapticFeedback.trigger("impactHeavy")} - accessibilityLabel="impactHeavy" - accessibilityHint="impactHeavy" - /> - - ReactNativeHapticFeedback.trigger("rigid")} - accessibilityLabel="rigid" - accessibilityHint="rigid" - /> - - ReactNativeHapticFeedback.trigger("soft")} - accessibilityLabel="soft" - accessibilityHint="soft" - /> - - - ReactNativeHapticFeedback.trigger("notificationSuccess") - } - accessibilityLabel="notificationSuccess" - accessibilityHint="notificationSuccess" - /> - - - ReactNativeHapticFeedback.trigger("notificationWarning") - } - accessibilityLabel="notificationWarning" - accessibilityHint="notificationWarning" - /> - - ReactNativeHapticFeedback.trigger("notificationError")} - accessibilityLabel="notificationError" - accessibilityHint="notificationError" - /> -
- )} -
-); +export const DSHapticFeedback = () => { + const theme = useIOTheme(); + return ( + +

Feedback

+ + ReactNativeHapticFeedback.trigger("impactLight")} + accessibilityLabel="impactLight" + accessibilityHint="impactLight" + /> + + ReactNativeHapticFeedback.trigger("impactMedium")} + accessibilityLabel="impactMedium" + accessibilityHint="impactMedium" + /> + + ReactNativeHapticFeedback.trigger("impactHeavy")} + accessibilityLabel="impactHeavy" + accessibilityHint="impactHeavy" + /> + + ReactNativeHapticFeedback.trigger("rigid")} + accessibilityLabel="rigid" + accessibilityHint="rigid" + /> + + ReactNativeHapticFeedback.trigger("soft")} + accessibilityLabel="soft" + accessibilityHint="soft" + /> + + ReactNativeHapticFeedback.trigger("notificationSuccess")} + accessibilityLabel="notificationSuccess" + accessibilityHint="notificationSuccess" + /> + + ReactNativeHapticFeedback.trigger("notificationWarning")} + accessibilityLabel="notificationWarning" + accessibilityHint="notificationWarning" + /> + + ReactNativeHapticFeedback.trigger("notificationError")} + accessibilityLabel="notificationError" + accessibilityHint="notificationError" + /> +
+ ); +}; diff --git a/ts/features/design-system/core/DSHeaderSecondLevel.tsx b/ts/features/design-system/core/DSHeaderSecondLevel.tsx index 1838a0a2276..89d44f1a5c2 100644 --- a/ts/features/design-system/core/DSHeaderSecondLevel.tsx +++ b/ts/features/design-system/core/DSHeaderSecondLevel.tsx @@ -3,7 +3,11 @@ import { Body, VSpacer } from "@pagopa/io-app-design-system"; import { RNavScreenWithLargeHeader } from "../../../components/ui/RNavScreenWithLargeHeader"; export const DSHeaderSecondLevel = () => ( - + {[...Array(50)].map((_el, i) => ( Repeated text diff --git a/ts/features/design-system/core/DSIcons.tsx b/ts/features/design-system/core/DSIcons.tsx index ffb2766e00a..05f08908c56 100644 --- a/ts/features/design-system/core/DSIcons.tsx +++ b/ts/features/design-system/core/DSIcons.tsx @@ -11,11 +11,11 @@ import { SVGIconProps, IOIconSizeScale, IOSystemIcons, - IOThemeContext, IOColors, IconContained, HSpacer, - IOStyles + IOStyles, + useIOTheme } from "@pagopa/io-app-design-system"; import { DSIconViewerBox, iconItemGutter } from "../components/DSIconViewerBox"; import { H2 } from "../../../components/core/typography/H2"; @@ -75,202 +75,201 @@ const styles = StyleSheet.create({ } }); -export const DSIcons = () => ( - - {theme => ( - - - {Object.entries(filteredIOIcons).map(([iconItemName]) => ( - - } - withDot={Object.keys(IOIconsNew).includes(iconItemName)} - /> - ))} - -

- Navigation -

- - {Object.entries(IONavIcons).map(([iconItemName]) => ( - - } - /> - ))} - -

- Biometric -

- - {Object.entries(IOBiometricIcons).map(([iconItemName]) => ( - - } - /> - ))} - -

- Categories -

- - {Object.entries(IOCategoryIcons).map(([iconItemName]) => ( - - } - /> - ))} - -

- Product -

- - {Object.entries(IOProductIcons).map(([iconItemName]) => ( - - } - /> - ))} - -

- System -

- - {Object.entries(IOSystemIcons).map(([iconItemName]) => ( - - } - /> - ))} - +export const DSIcons = () => { + const theme = useIOTheme(); + return ( + + + {Object.entries(filteredIOIcons).map(([iconItemName]) => ( + + } + withDot={Object.keys(IOIconsNew).includes(iconItemName)} + /> + ))} + +

+ Navigation +

+ + {Object.entries(IONavIcons).map(([iconItemName]) => ( + + } + /> + ))} + +

+ Biometric +

+ + {Object.entries(IOBiometricIcons).map(([iconItemName]) => ( + + } + /> + ))} + +

+ Categories +

+ + {Object.entries(IOCategoryIcons).map(([iconItemName]) => ( + + } + /> + ))} + +

+ Product +

+ + {Object.entries(IOProductIcons).map(([iconItemName]) => ( + + } + /> + ))} + +

+ System +

+ + {Object.entries(IOSystemIcons).map(([iconItemName]) => ( + + } + /> + ))} + -

- IconContained -

- - - - - - - +

+ IconContained +

+ + + + + + + -

- Sizes -

- - {/* If you want to render another icon in different sizes, +

+ Sizes +

+ + {/* If you want to render another icon in different sizes, just change the name below */} - {IOIconSizes.map(size => ( - - } - /> - ))} - -

- Colors -

- - {IOIconColors.map(color => ( - - } - /> - ))} - -
- )} -
-); + {IOIconSizes.map(size => ( + + } + /> + ))} + +

+ Colors +

+ + {IOIconColors.map(color => ( + + } + /> + ))} + + + ); +}; diff --git a/ts/features/design-system/core/DSLayout.tsx b/ts/features/design-system/core/DSLayout.tsx index 8b5ea692ec8..3a9257d545a 100644 --- a/ts/features/design-system/core/DSLayout.tsx +++ b/ts/features/design-system/core/DSLayout.tsx @@ -2,14 +2,14 @@ import * as React from "react"; import { View } from "react-native"; import { IOColors, - IOThemeContext, Divider, VDivider, HSpacer, VSpacer, IOAppMargin, IOSpacer, - ContentWrapper + ContentWrapper, + useIOTheme } from "@pagopa/io-app-design-system"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; import { DSSpacerViewerBox } from "../components/DSSpacerViewerBox"; @@ -18,152 +18,147 @@ import { LabelSmall } from "../../../components/core/typography/LabelSmall"; import { H3 } from "../../../components/core/typography/H3"; import { H1 } from "../../../components/core/typography/H1"; -export const DSLayout = () => ( - - {theme => ( - - -

- Grid -

-

{ + const theme = useIOTheme(); + + return ( + + +

+ Grid +

+

+ ContentWrapper +

+
+ {IOAppMargin.map((value, i, arr) => ( + + - ContentWrapper -

-
- {IOAppMargin.map((value, i, arr) => ( - - - - + + Content example + - - Content example - - - {value} - - - - - {i !== arr.length - 1 && } - - ))} + {value} + + + + + {i !== arr.length - 1 && } + + ))} - + - -

- Spacing -

+ +

+ Spacing +

-

- VSpacer -

+

+ VSpacer +

- {/* Vertical */} - {IOSpacer.map((spacerEntry, i, arr) => ( - - - {/* Don't add spacer to the last item. Quick and dirty + {/* Vertical */} + {IOSpacer.map((spacerEntry, i, arr) => ( + + + {/* Don't add spacer to the last item. Quick and dirty alternative to the Stack component. https://stackoverflow.com/a/60975451 */} - {i !== arr.length - 1 && } - - ))} + {i !== arr.length - 1 && } + + ))} - + -

- HSpacer -

+

+ HSpacer +

- {/* Horizontal */} - - {IOSpacer.map((spacerEntry, i, arr) => ( - - - {i !== arr.length - 1 && } - - ))} - + {/* Horizontal */} + + {IOSpacer.map((spacerEntry, i, arr) => ( + + + {i !== arr.length - 1 && } + + ))} + - -
+ +
- -

- Divider -

+ +

+ Divider +

-

- Default (Horizontal) -

+

+ Default (Horizontal) +

- - -
+
+ + - -

- Vertical -

+ +

+ Vertical +

- - - - -
-
- )} -
-); + + + + + + + ); +}; diff --git a/ts/features/design-system/core/DSLegacyButtons.tsx b/ts/features/design-system/core/DSLegacyButtons.tsx deleted file mode 100644 index 6658178d02e..00000000000 --- a/ts/features/design-system/core/DSLegacyButtons.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import * as React from "react"; -import { View } from "react-native"; -import { Text as NBText } from "native-base"; -import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import { H2 } from "../../../components/core/typography/H2"; - -import { DesignSystemScreen } from "../components/DesignSystemScreen"; -import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; -import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; -import { Label } from "../../../components/core/typography/Label"; -import GoBackButton from "../../../components/GoBackButton"; -import { LabelSmall } from "../../../components/core/typography/LabelSmall"; - -export const DSLegacyButtons = () => ( - -

- NativeBase -

- - {/* The following props render the button with the same - graphical attributes: - - Active - - Input Button - */} - { - alert("Action triggered"); - }} - > - Primary button - - - - { - alert("Action triggered"); - }} - > - Primary button (Block) - - - - { - alert("Action triggered"); - }} - > - Primary button (Small) - - - - { - alert("Action triggered"); - }} - > - XSmall button - - - - { - alert("Action triggered"); - }} - > - Primary button (Disabled) - - - - { - alert("Action triggered"); - }} - > - Outline button (Light) - - - - { - alert("Action triggered"); - }} - > - Outline button (Block Light) - - - - { - alert("Action triggered"); - }} - > - Outline button (Small Light) - - - - { - alert("Action triggered"); - }} - > - Outline button (disabled) - - - - { - alert("Action triggered"); - }} - > - Outline button (light) - - - - { - alert("Action triggered"); - }} - > - Outline button (block light) - - - - { - alert("Action triggered"); - }} - > - Outline button (small light) - - - - { - alert("Action triggered"); - }} - > - Outline button (light disabled) - - - - { - alert("Action triggered"); - }} - > - Primary button (dark) - - - - { - alert("Action triggered"); - }} - > - Outline button (dark) - - - - { - alert("Action triggered"); - }} - > - - - - - { - alert("Action triggered"); - }} - > - - - - - { - alert("Action triggered"); - }} - > - - - - - { - alert("Action triggered"); - }} - > - - - - - { - alert("Action triggered"); - }} - > - Button (light text) - - - - - { - alert("Action triggered"); - }} - > - - White button - - - - - - { - alert("Going back"); - }} - white={true} - /> - - - -
-); diff --git a/ts/features/design-system/core/DSLegacyIllustrations.tsx b/ts/features/design-system/core/DSLegacyIllustrations.tsx index 5ceef2b68b4..a0f001b9ecb 100644 --- a/ts/features/design-system/core/DSLegacyIllustrations.tsx +++ b/ts/features/design-system/core/DSLegacyIllustrations.tsx @@ -8,14 +8,7 @@ import { } from "../components/DSAssetViewerBox"; /* ILLUSTRATIONS */ -/* Onboarding */ -import Landing05 from "../../../../img/landing/05.png"; -import Landing01 from "../../../../img/landing/01.png"; -import Landing02 from "../../../../img/landing/02.png"; -import Landing03 from "../../../../img/landing/03.png"; -import Landing04 from "../../../../img/landing/04.png"; /* CIE */ -import LandingCIE from "../../../../img/cie/CIE-onboarding-illustration.png"; import PlacingCard from "../../../../img/cie/place-card-illustration.png"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; @@ -32,45 +25,10 @@ const styles = StyleSheet.create({ export const DSLegacyIllustrations = () => ( -

- Onboarding -

- - - - - - -

CIE

- { return ( - - - { name={"ABILogo"} image={renderRasterImage(ABILogo)} /> - - - - { name={"Completed"} image={renderRasterImage(CompletedRaster)} /> - } /> { Alert.alert("Copied!", "Value copied"); }; -export const DSListItems = () => ( - - {theme => ( - -

- ListItemNav -

- {renderListItemNav()} - -

- ListItemInfoCopy -

- {renderListItemInfoCopy()} - -

- ListItemInfo -

- {renderListItemInfo()} - -

- ListItemHeader -

- {renderListItemHeader()} - -

- ListItemAction -

- {renderListItemAction()} - -

- ListItemTransaction -

- {renderListItemTransaction()} - - - -

- NativeBase lookalikes (not NativeBase) -

- - alert("Action triggered")} - /> - - - - -

- ListItemComponent (NativeBase) -

- - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - hideSeparator={true} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - - alert("Action triggered")} - /> - - -

- Derivated from ListItem (NativeBase) -

- - alert("Action triggered")} - /> - alert("Action triggered")} - /> - - - - } - title="Storico versioni dell'app" - value="Per capire se il problema dipende dall'ultimo aggiornamento" - testId="TestID" - /> - -

- Misc -

- - alert("Action triggered")} - accessible={true} - accessibilityRole={"button"} - accessibilityLabel={"Accessibility Label"} - /> - - -

- Native (Not NativeBase) -

- - - - - alert("Action triggered")} - isNew={true} - /> - -
- )} -
-); +export const DSListItems = () => { + const theme = useIOTheme(); + return ( + +

+ ListItemNav +

+ {renderListItemNav()} + +

+ ListItemInfoCopy +

+ {renderListItemInfoCopy()} + +

+ ListItemInfo +

+ {renderListItemInfo()} + +

+ ListItemHeader +

+ {renderListItemHeader()} + +

+ ListItemAction +

+ {renderListItemAction()} + +

+ ListItemTransaction +

+ {renderListItemTransaction()} + + + +

+ NativeBase lookalikes (not NativeBase) +

+ + alert("Action triggered")} + /> + + + + +

+ ListItemComponent (NativeBase) +

+ + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + hideSeparator={true} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + + alert("Action triggered")} + /> + + +

+ Derivated from ListItem (NativeBase) +

+ + alert("Action triggered")} + /> + alert("Action triggered")} + /> + + + + } + title="Storico versioni dell'app" + value="Per capire se il problema dipende dall'ultimo aggiornamento" + testId="TestID" + /> + +

+ Misc +

+ + alert("Action triggered")} + accessible={true} + accessibilityRole={"button"} + accessibilityLabel={"Accessibility Label"} + /> + + +

+ Native (Not NativeBase) +

+ + + + + alert("Action triggered")} + isNew={true} + /> + +
+ ); +}; const renderListItemNav = () => ( <> diff --git a/ts/features/design-system/core/DSLogos.tsx b/ts/features/design-system/core/DSLogos.tsx index b9baf5982a1..05f3373743f 100644 --- a/ts/features/design-system/core/DSLogos.tsx +++ b/ts/features/design-system/core/DSLogos.tsx @@ -146,31 +146,7 @@ const organizationsURIs = [ const renderAvatar = () => ( <> - - - {organizationsURIs.map(({ imageSource }, i) => ( - - - {i < organizationsURIs.length - 1 && } - - ))} - - - + ( ))} - + { Alert.alert("Alert", "Action triggered"); @@ -32,113 +35,158 @@ const noticeStatusArray: Array = [ "canceled" ]; -export const DSModules = () => ( - - {theme => ( - -

- ModuleAttachment -

- {renderModuleAttachment()} +export const DSModules = () => { + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + const theme = useIOTheme(); + return ( + +

+ ModuleAttachment +

+ {renderModuleAttachment(isDesignSystemEnabled)} - + -

- ModulePaymentNotice -

- {renderModulePaymentNotice()} +

+ ModulePaymentNotice +

+ {renderModulePaymentNotice()} - + -

- ModuleCheckout -

- {renderModuleCheckout()} +

+ ModuleCheckout +

+ {renderModuleCheckout()} - + -

- ButtonExtendedOutline -

- {renderButtonExtendedOutline()} +

+ ButtonExtendedOutline +

+ {renderButtonExtendedOutline()} - + -

- ModuleIDP -

- {renderModuleIDP()} -
- )} -
-); +

+ ModuleIDP +

+ {renderModuleIDP()} +
+ ); +}; -const renderModuleAttachment = () => ( +const renderModuleAttachment = (isDesignSystemEnabled: boolean) => ( - + {isDesignSystemEnabled ? ( + + ) : ( + + )} - + {isDesignSystemEnabled ? ( + + ) : ( + + )} - + {isDesignSystemEnabled ? null : ( + + )} - + {isDesignSystemEnabled ? ( + + ) : ( + + )} - + {isDesignSystemEnabled ? ( + + ) : ( + + )} - + {isDesignSystemEnabled ? ( + + ) : ( + + )} ); diff --git a/ts/features/design-system/core/DSNumberPad.tsx b/ts/features/design-system/core/DSNumberPad.tsx index 26a9fe60bd6..50ab287d3a2 100644 --- a/ts/features/design-system/core/DSNumberPad.tsx +++ b/ts/features/design-system/core/DSNumberPad.tsx @@ -1,20 +1,20 @@ -import * as React from "react"; import { + Body, CodeInput, H2, IOColors, - ListItemSwitch, - NumberPad, - VSpacer, - hexToRgba, - LabelSmallAlt, IOVisualCostants, LabelLink, + LabelSmallAlt, + ListItemSwitch, + NumberPad, Pictogram, - Body + VSpacer, + hexToRgba } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; -import { Alert, View } from "react-native"; +import * as React from "react"; +import { Alert, StatusBar, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; const PIN_LENGTH = 6; @@ -59,6 +59,7 @@ export const DSNumberPad = () => { } ]} > + { - const theme = useContext(IOThemeContext); + const theme = useIOTheme(); return ( diff --git a/ts/features/design-system/core/DSScreenOperationResult.tsx b/ts/features/design-system/core/DSScreenOperationResult.tsx index 655253035f0..ba57b15d5a6 100644 --- a/ts/features/design-system/core/DSScreenOperationResult.tsx +++ b/ts/features/design-system/core/DSScreenOperationResult.tsx @@ -1,16 +1,41 @@ import { useNavigation } from "@react-navigation/native"; import React from "react"; -import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; +import { + BodyProps, + OperationResultScreenContent +} from "../../../components/screens/OperationResultScreenContent"; import I18n from "../../../i18n"; const DSScreenOperationResult = () => { const navigation = useNavigation(); + const bodyPropsArray: Array = [ + { + text: I18n.t("email.cduScreens.emailAlreadyTaken.subtitleStart"), + style: { + textAlign: "center" + } + }, + { + text: <> example@try.com , + style: { + textAlign: "center" + }, + weight: "SemiBold" + }, + { + text: I18n.t("email.cduScreens.emailAlreadyTaken.subtitleEnd"), + style: { + textAlign: "center" + } + } + ]; + return ( > => [ "Ti contatteranno solo i servizi che hanno qualcosa di importante da dirti. Potrai sempre disattivare le comunicazioni che non ti interessano.", id: "example-1" }, + { + value: "Let's try with JSX description", + description: ( + + Ti contatteranno solo i servizi che hanno qualcosa di importante da + dirti.{" "} + + Potrai sempre disattivare le comunicazioni che non ti interessano. + + + ), + id: "example-jsx-element" + }, { startImage: { paymentLogo: "myBank" }, value: "Payment method item", diff --git a/ts/features/design-system/core/DSToastNotifications.tsx b/ts/features/design-system/core/DSToastNotifications.tsx index 90b92ded4b0..ff7c3887259 100644 --- a/ts/features/design-system/core/DSToastNotifications.tsx +++ b/ts/features/design-system/core/DSToastNotifications.tsx @@ -1,11 +1,12 @@ -import { Toast as NBToast } from "native-base"; +import { + ButtonOutline, + VSpacer, + IOToast, + ToastNotification +} from "@pagopa/io-app-design-system"; import * as React from "react"; -import { ButtonOutline, VSpacer } from "@pagopa/io-app-design-system"; -import { H2 } from "../../../components/core/typography/H2"; import { H3 } from "../../../components/core/typography/H3"; -import { showToast } from "../../../utils/showToast"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; -import { IOToast, ToastNotification } from "../../../components/Toast"; export const DSToastNotifications = () => ( @@ -88,111 +89,5 @@ export const DSToastNotifications = () => ( /> - -

- Legacy toasts -

- -

- Type -

- - - NBToast.show({ - text: "Here's the default behavior" - }) - } - /> - - - - showToast("Example of a danger message")} - /> - - - - showToast("Example of a success message", "success")} - /> - - - - showToast("Example of a warning message", "warning")} - /> - -

- Position -

- - - showToast("Here's the notification at the top", "danger", "top") - } - /> - - - - - showToast("Here's the notification at the top", "danger", "top") - } - /> - - - - - showToast("Here's the notification at the bottom", "danger", "bottom") - } - /> - -

- Misc -

- - - NBToast.show({ - text: "Here's the default behavior with multi-line loooong loooooong text" - }) - } - /> - -
); diff --git a/ts/features/design-system/core/DSWallet.tsx b/ts/features/design-system/core/DSWallet.tsx new file mode 100644 index 00000000000..d26fc64fba9 --- /dev/null +++ b/ts/features/design-system/core/DSWallet.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import { WalletCardsCategoryContainer } from "../../newWallet/components/WalletCardsCategoryContainer"; +import { WalletCard, WalletCardCategory } from "../../newWallet/types"; +import { DesignSystemScreen } from "../components/DesignSystemScreen"; +import { DesignSystemSection } from "../components/DesignSystemSection"; + +export const DSWallet = () => { + const cards: ReadonlyArray = [ + { + key: "1", + type: "idPay", + category: "bonus", + initiativeId: "1", + amount: 412.69, + avatarSource: { + uri: "https://vtlogo.com/wp-content/uploads/2021/08/18app-vector-logo.png" + }, + expireDate: new Date(), + name: "18 App" + }, + { + key: "2", + type: "payment", + category: "payment", + walletId: "1", + hpan: "9900", + brand: "maestro", + holderName: "Anna Verdi", + expireDate: new Date() + }, + { + key: "3", + type: "payment", + category: "payment", + walletId: "1", + abiCode: "03069", + brand: "pagoBancomat", + holderName: "Anna Verdi", + expireDate: new Date() + }, + { + key: "4", + type: "payment", + category: "payment", + walletId: "1", + holderEmail: "anna_v********@**hoo.it" + } + ]; + + const cardsByCategory = cards.reduce( + (acc, card) => ({ + ...acc, + [card.category]: [...(acc[card.category] || []), card] + }), + {} as { [category in WalletCardCategory]: ReadonlyArray } + ); + + return ( + + + + + + + + + + ); +}; diff --git a/ts/features/design-system/navigation/navigator.tsx b/ts/features/design-system/navigation/navigator.tsx index ca02ca4ee8f..3f3f21746a3 100644 --- a/ts/features/design-system/navigation/navigator.tsx +++ b/ts/features/design-system/navigation/navigator.tsx @@ -1,26 +1,16 @@ import { - IOThemeContext, - IOThemes, - IOVisualCostants, - IconButton + HeaderSecondLevel, + IconButton, + makeFontStyleObject, + useIOExperimentalDesign, + useIOThemeContext } from "@pagopa/io-app-design-system"; -import { ThemeProvider, useNavigation } from "@react-navigation/native"; -import { - StackNavigationOptions, - TransitionPresets, - createStackNavigator -} from "@react-navigation/stack"; +import { useNavigation } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import I18n from "i18n-js"; import * as React from "react"; -import { useMemo } from "react"; -import { Alert, Platform, View, useColorScheme } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { makeFontStyleObject } from "../../../components/core/fonts"; +import { Alert, Platform } from "react-native"; import HeaderFirstLevel from "../../../components/ui/HeaderFirstLevel"; -import { - IONavigationDarkTheme, - IONavigationLightTheme -} from "../../../theme/navigations"; -import { isGestureEnabled } from "../../../utils/navigation"; import { DesignSystem } from "../DesignSystem"; import { DSAccordion } from "../core/DSAccordion"; import { DSAdvice } from "../core/DSAdvice"; @@ -38,16 +28,15 @@ import { DSHapticFeedback } from "../core/DSHapticFeedback"; import { DSHeaderFirstLevel } from "../core/DSHeaderFirstLevel"; import { DSHeaderSecondLevel } from "../core/DSHeaderSecondLevel"; import { DSHeaderSecondLevelWithSectionTitle } from "../core/DSHeaderSecondLevelWithSectionTitle"; -import { DSNumberPad } from "../core/DSNumberPad"; import { DSIcons } from "../core/DSIcons"; import { DSLayout } from "../core/DSLayout"; -import { DSLegacyButtons } from "../core/DSLegacyButtons"; import { DSLegacyIllustrations } from "../core/DSLegacyIllustrations"; import { DSLegacyPictograms } from "../core/DSLegacyPictograms"; import { DSListItems } from "../core/DSListItems"; import { DSLoaders } from "../core/DSLoaders"; import { DSLogos } from "../core/DSLogos"; import { DSModules } from "../core/DSModules"; +import { DSNumberPad } from "../core/DSNumberPad"; import { DSPictograms } from "../core/DSPictograms"; import { DSSafeArea } from "../core/DSSafeArea"; import { DSSafeAreaCentered } from "../core/DSSafeAreaCentered"; @@ -57,46 +46,26 @@ import { DSTabNavigation } from "../core/DSTabNavigation"; import { DSTextFields } from "../core/DSTextFields"; import { DSToastNotifications } from "../core/DSToastNotifications"; import { DSTypography } from "../core/DSTypography"; +import { DSWallet } from "../core/DSWallet"; import { DSWizardScreen } from "../core/DSWizardScreen"; -import { DesignSystemModalParamsList, DesignSystemParamsList } from "./params"; +import { DesignSystemParamsList } from "./params"; import DESIGN_SYSTEM_ROUTES from "./routes"; -const Stack = createStackNavigator(); -const ModalStack = createStackNavigator(); +const Stack = createNativeStackNavigator(); // BackButton managed through React Navigation const RNNBackButton = () => { const navigation = useNavigation(); - const colorScheme = useColorScheme(); - + const { themeType } = useIOThemeContext(); return ( - - { - navigation.goBack(); - }} - accessibilityLabel={""} - /> - - ); -}; - -const RNNCloseButton = () => { - const navigation = useNavigation(); - - return ( - - { - navigation.goBack(); - }} - accessibilityLabel={""} - /> - + { + navigation.goBack(); + }} + accessibilityLabel={""} + /> ); }; @@ -124,89 +93,33 @@ const HeaderFirstLevelComponent = () => ( /> ); -const customModalHeaderConf: StackNavigationOptions = { - headerLeft: () => null, - headerTitle: () => null, - headerRight: RNNCloseButton, - headerStyle: { height: IOVisualCostants.headerHeight }, - headerStatusBarHeight: 0 -}; - export const DesignSystemNavigator = () => { - const colorScheme = useColorScheme(); - - return ( - - - {/* You need two nested navigators to apply the modal - behavior only to the single screen and not to any other. - Read documentation for reference: - https://reactnavigation.org/docs/5.x/modal/#creating-a-modal-stack - - With RN Navigation 6.x it's much easier because you can - use the Group function */} - - - - - - - ); -}; - -const DesignSystemMainStack = () => { - const insets = useSafeAreaInsets(); - - const customHeaderConf: StackNavigationOptions = useMemo( - () => ({ - headerTitleStyle: { - ...makeFontStyleObject("Regular", false, "ReadexPro"), - fontSize: 14 - }, - headerTitleAlign: "center", - headerStyle: { height: insets.top + IOVisualCostants.headerHeight }, - headerLeft: RNNBackButton - }), - [insets] - ); + const { isExperimental } = useIOExperimentalDesign(); return ( - @@ -214,7 +127,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.COLOR.route} component={DSColors} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.COLOR.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.COLOR.title }} /> @@ -222,7 +135,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.TYPOGRAPHY.route} component={DSTypography} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.TYPOGRAPHY.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.TYPOGRAPHY.title }} /> @@ -230,7 +143,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.LAYOUT.route} component={DSLayout} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.LAYOUT.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.LAYOUT.title }} /> @@ -238,7 +151,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.ICONS.route} component={DSIcons} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.ICONS.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.ICONS.title }} /> @@ -246,7 +159,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.PICTOGRAMS.route} component={DSPictograms} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.PICTOGRAMS.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.PICTOGRAMS.title }} /> @@ -254,7 +167,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.LOGOS.route} component={DSLogos} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.LOGOS.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.LOGOS.title }} /> @@ -262,7 +175,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.LOADERS.route} component={DSLoaders} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.LOADERS.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.LOADERS.title }} /> @@ -270,7 +183,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.FOUNDATION.HAPTIC_FEEDBACK.route} component={DSHapticFeedback} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.FOUNDATION.HAPTIC_FEEDBACK.title + title: DESIGN_SYSTEM_ROUTES.FOUNDATION.HAPTIC_FEEDBACK.title }} /> @@ -279,7 +192,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.BUTTONS.route} component={DSButtons} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.BUTTONS.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.BUTTONS.title }} /> @@ -287,7 +200,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.SELECTION.route} component={DSSelection} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.SELECTION.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.SELECTION.title }} /> @@ -295,7 +208,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.TEXT_FIELDS.route} component={DSTextFields} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.TEXT_FIELDS.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.TEXT_FIELDS.title }} /> @@ -303,7 +216,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.BADGE.route} component={DSBadges} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.BADGE.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.BADGE.title }} /> @@ -311,7 +224,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.LIST_ITEMS.route} component={DSListItems} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.LIST_ITEMS.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.LIST_ITEMS.title }} /> @@ -319,7 +232,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.MODULES.route} component={DSModules} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.MODULES.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.MODULES.title }} /> @@ -327,14 +240,15 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.CARDS.route} component={DSCards} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.CARDS.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.CARDS.title }} /> + @@ -342,7 +256,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.ACCORDION.route} component={DSAccordion} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.ACCORDION.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.ACCORDION.title }} /> @@ -350,7 +264,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.ALERT.route} component={DSAlert} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.ALERT.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.ALERT.title }} /> @@ -358,7 +272,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.ADVICE.route} component={DSAdvice} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.ADVICE.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.ADVICE.title }} /> @@ -366,7 +280,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.BOTTOM_SHEET.route} component={DSBottomSheet} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.BOTTOM_SHEET.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.BOTTOM_SHEET.title }} /> @@ -374,7 +288,15 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.COMPONENTS.TAB_NAVIGATION.route} component={DSTabNavigation} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.COMPONENTS.TAB_NAVIGATION.title + title: DESIGN_SYSTEM_ROUTES.COMPONENTS.TAB_NAVIGATION.title + }} + /> + + @@ -405,7 +327,7 @@ const DesignSystemMainStack = () => { name={DESIGN_SYSTEM_ROUTES.SCREENS.GRADIENT_SCROLL.route} component={DSGradientScroll} options={{ - headerTitle: DESIGN_SYSTEM_ROUTES.SCREENS.GRADIENT_SCROLL.title + title: DESIGN_SYSTEM_ROUTES.SCREENS.GRADIENT_SCROLL.title }} /> @@ -439,37 +361,60 @@ const DesignSystemMainStack = () => { component={DSSafeArea} options={{ headerShown: false }} /> + + + + ( + { + navigation.goBack(); + }, + accessibilityLabel: I18n.t("global.buttons.back") + }} + /> + ) + }} + /> + + {/* LEGACY */} - diff --git a/ts/features/design-system/navigation/params.ts b/ts/features/design-system/navigation/params.ts index bae589ccdbe..811d196304b 100644 --- a/ts/features/design-system/navigation/params.ts +++ b/ts/features/design-system/navigation/params.ts @@ -22,6 +22,7 @@ export type DesignSystemParamsList = { [DESIGN_SYSTEM_ROUTES.COMPONENTS.TOASTS.route]: undefined; [DESIGN_SYSTEM_ROUTES.COMPONENTS.BOTTOM_SHEET.route]: undefined; [DESIGN_SYSTEM_ROUTES.COMPONENTS.TAB_NAVIGATION.route]: undefined; + [DESIGN_SYSTEM_ROUTES.COMPONENTS.WALLET.route]: undefined; [DESIGN_SYSTEM_ROUTES.HEADERS.FIRST_LEVEL.route]: undefined; [DESIGN_SYSTEM_ROUTES.HEADERS.SECOND_LEVEL.route]: undefined; [DESIGN_SYSTEM_ROUTES.HEADERS.SECOND_LEVEL_SECTION_TITLE.route]: undefined; @@ -34,12 +35,7 @@ export type DesignSystemParamsList = { [DESIGN_SYSTEM_ROUTES.DEBUG.SAFE_AREA.route]: undefined; [DESIGN_SYSTEM_ROUTES.DEBUG.SAFE_AREA_CENTERED.route]: undefined; [DESIGN_SYSTEM_ROUTES.DEBUG.EDGE_TO_EDGE_AREA.route]: undefined; + [DESIGN_SYSTEM_ROUTES.DEBUG.FULL_SCREEN_MODAL.route]: undefined; [DESIGN_SYSTEM_ROUTES.LEGACY.PICTOGRAMS.route]: undefined; - [DESIGN_SYSTEM_ROUTES.LEGACY.BUTTONS.route]: undefined; [DESIGN_SYSTEM_ROUTES.LEGACY.ILLUSTRATIONS.route]: undefined; }; - -export type DesignSystemModalParamsList = { - [DESIGN_SYSTEM_ROUTES.MAIN.route]: undefined; - [DESIGN_SYSTEM_ROUTES.DEBUG.FULL_SCREEN_MODAL.route]: undefined; -}; diff --git a/ts/features/design-system/navigation/routes.ts b/ts/features/design-system/navigation/routes.ts index 48ceca64e34..4baccfa8b81 100644 --- a/ts/features/design-system/navigation/routes.ts +++ b/ts/features/design-system/navigation/routes.ts @@ -29,6 +29,10 @@ const DESIGN_SYSTEM_ROUTES = { TAB_NAVIGATION: { route: "DESIGN_SYSTEM_TAB_NAVIGATION", title: "Tab Navigation" + }, + WALLET: { + route: "DESIGN_SYSTEM_WALLET", + title: "Wallet" } }, HEADERS: { @@ -84,10 +88,6 @@ const DESIGN_SYSTEM_ROUTES = { ILLUSTRATIONS: { route: "DESIGN_SYSTEM_LEGACY_ILLUSTRATIONS", title: "Illustrations" - }, - BUTTONS: { - route: "DESIGN_SYSTEM_LEGACY_BUTTONS", - title: "Buttons" } } as const } as const; diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertExpired00.e2e.ts similarity index 63% rename from ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts rename to ts/features/euCovidCert/__e2e__/euCovidCertExpired00.e2e.ts index 84b19fee096..7275f114f4d 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertExpired00.e2e.ts @@ -1,31 +1,21 @@ import { device } from "detox"; -import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { learnMoreLinkTestId, scrollToEUCovidMessage } from "./utils"; const euCovidCertExpiredSubject = "🏥 EUCovidCert - expired"; const euCovidCertExpiredTitle = "Expired Certificate title"; const euCovidCertExpiredSubTitle = "Expired Certificate sub title"; -const messageListTestId = "MessageList_inbox"; -const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; - describe("EuCovidCert Expired", () => { beforeAll(async () => { await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); - it("should find the expired EuCovidCert message and open it", async () => { - await waitFor(element(by.text(euCovidCertExpiredSubject))) - .toBeVisible() - .whileElement(by.id(messageListTestId)) - .scroll(350, "down"); - - const subject = element(by.text(euCovidCertExpiredSubject)); - await subject.tap(); - }); + it("should find the expired EuCovidCert message, open it and check all the correct elements in the details page", async () => { + await openExpiredEUCovidMessage(); - it("should check all the correct elements in the details page", async () => { await waitFor(element(by.text(euCovidCertExpiredTitle))) .toBeVisible() .withTimeout(e2eWaitRenderTimeout); @@ -39,3 +29,10 @@ describe("EuCovidCert Expired", () => { .withTimeout(e2eWaitRenderTimeout); }); }); + +const openExpiredEUCovidMessage = async () => { + await scrollToEUCovidMessage(euCovidCertExpiredSubject); + + const subject = element(by.text(euCovidCertExpiredSubject)); + await subject.tap(); +}; diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked00.e2e.ts similarity index 63% rename from ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts rename to ts/features/euCovidCert/__e2e__/euCovidCertRevoked00.e2e.ts index c2de8638b12..d0a0e9cc314 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked00.e2e.ts @@ -1,31 +1,21 @@ import { device } from "detox"; -import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { learnMoreLinkTestId, scrollToEUCovidMessage } from "./utils"; const euCovidCertRevokedSubject = "🏥 EUCovidCert - revoked"; const euCovidCertRevokedTitle = "Revoked Certificate title"; const euCovidCertRevokedSubTitle = "Revoked Certificate sub title"; -const messageListTestId = "MessageList_inbox"; -const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; - describe("EuCovidCert Revoked", () => { beforeAll(async () => { await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); - it("should find the revoked EuCovidCert message and open it", async () => { - await waitFor(element(by.text(euCovidCertRevokedSubject))) - .toBeVisible() - .whileElement(by.id(messageListTestId)) - .scroll(350, "down"); - - const subject = element(by.text(euCovidCertRevokedSubject)); - await subject.tap(); - }); + it("should find the revoked EuCovidCert message, open it and check all the correct elements in the details page", async () => { + await openRevokedEUCovidMessage(); - it("should check all the correct elements in the details page", async () => { await waitFor(element(by.text(euCovidCertRevokedTitle))) .toBeVisible() .withTimeout(e2eWaitRenderTimeout); @@ -39,3 +29,10 @@ describe("EuCovidCert Revoked", () => { .withTimeout(e2eWaitRenderTimeout); }); }); + +const openRevokedEUCovidMessage = async () => { + await scrollToEUCovidMessage(euCovidCertRevokedSubject); + + const subject = element(by.text(euCovidCertRevokedSubject)); + await subject.tap(); +}; diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts deleted file mode 100644 index 8f2fe2ac19b..00000000000 --- a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { device } from "detox"; -import I18n from "../../../i18n"; -import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; -import { ensureLoggedIn } from "../../../__e2e__/utils"; - -const euCovidCertValidSubject = "🏥 EUCovidCert - valid"; -const euCovidCertValidTitle = "Valid Certificate title"; -const euCovidCertValidSubTitle = "Valid Certificate sub title"; - -const messageListTestId = "MessageList_inbox"; -const qrCodeTestId = "QRCode"; -const fullScreenQrCodeTestId = "fullScreenQRCode"; - -describe("EuCovidCert Valid", () => { - beforeAll(async () => { - await device.launchApp({ newInstance: true }); - await ensureLoggedIn(); - }); - - it("should find the valid EuCovidCert message and open it", async () => { - await waitFor(element(by.text(euCovidCertValidSubject))) - .toBeVisible() - .whileElement(by.id(messageListTestId)) - .scroll(350, "down"); - - const subject = element(by.text(euCovidCertValidSubject)); - await subject.tap(); - }); - - it("should check all the correct elements in the details page", async () => { - await waitFor(element(by.text(euCovidCertValidTitle))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - await waitFor(element(by.text(euCovidCertValidSubTitle))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - await waitFor(element(by.text(I18n.t("global.genericSave")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - await waitFor(element(by.id(qrCodeTestId))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); - - it("should open the QRCode in fullscreen and return back", async () => { - const qrCode = element(by.id(qrCodeTestId)); - await qrCode.tap(); - - await waitFor(element(by.id(fullScreenQrCodeTestId))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - await waitFor(element(by.text(I18n.t("global.buttons.close")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const closeButton = element(by.text(I18n.t("global.buttons.close"))); - await closeButton.tap(); - }); - - it("should open the certificate details page and return back", async () => { - await waitFor(element(by.text(I18n.t("global.buttons.details")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const detailsButton = element(by.text(I18n.t("global.buttons.details"))); - await detailsButton.tap(); - - await waitFor(element(by.text(I18n.t("global.buttons.close")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const closeButton = element(by.text(I18n.t("global.buttons.close"))); - await closeButton.tap(); - }); - - it("should save the certificate in the gallery", async () => { - await waitFor(element(by.text(I18n.t("global.genericSave")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const saveButton = element(by.text(I18n.t("global.genericSave"))); - await saveButton.tap(); - - await waitFor( - element( - by.text( - I18n.t( - "features.euCovidCertificate.save.bottomSheet.saveAsImage.title" - ) - ) - ) - ) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - const saveIntoGalleryButton = element( - by.text( - I18n.t("features.euCovidCertificate.save.bottomSheet.saveAsImage.title") - ) - ); - await saveIntoGalleryButton.tap(); - - await waitFor( - element(by.text(I18n.t("features.euCovidCertificate.save.ok"))) - ) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); -}); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid00.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid00.e2e.ts new file mode 100644 index 00000000000..c76e0ebc284 --- /dev/null +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid00.e2e.ts @@ -0,0 +1,35 @@ +import { device } from "detox"; +import I18n from "../../../i18n"; +import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { openValidEUCovidMessage, qrCodeTestId } from "./utils"; + +const euCovidCertValidTitle = "Valid Certificate title"; +const euCovidCertValidSubTitle = "Valid Certificate sub title"; + +describe("EuCovidCert Valid", () => { + beforeAll(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("should find the valid EuCovidCert message, open it and check all the correct elements in the details page", async () => { + await openValidEUCovidMessage(); + + await waitFor(element(by.text(euCovidCertValidTitle))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + await waitFor(element(by.text(euCovidCertValidSubTitle))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + await waitFor(element(by.text(I18n.t("global.genericSave")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + await waitFor(element(by.id(qrCodeTestId))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + }); +}); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid01.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid01.e2e.ts new file mode 100644 index 00000000000..b2d36406d8b --- /dev/null +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid01.e2e.ts @@ -0,0 +1,34 @@ +import { device } from "detox"; +import I18n from "../../../i18n"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { + fullScreenQrCodeTestId, + openValidEUCovidMessage, + qrCodeTestId +} from "./utils"; + +describe("EuCovidCert Valid", () => { + beforeAll(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("should open the QRCode in fullscreen and return back", async () => { + await openValidEUCovidMessage(); + + const qrCode = element(by.id(qrCodeTestId)); + await qrCode.tap(); + + await waitFor(element(by.id(fullScreenQrCodeTestId))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + await waitFor(element(by.text(I18n.t("global.buttons.close")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const closeButton = element(by.text(I18n.t("global.buttons.close"))); + await closeButton.tap(); + }); +}); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid02.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid02.e2e.ts new file mode 100644 index 00000000000..964a37c6645 --- /dev/null +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid02.e2e.ts @@ -0,0 +1,30 @@ +import { device } from "detox"; +import I18n from "../../../i18n"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { openValidEUCovidMessage } from "./utils"; + +describe("EuCovidCert Valid", () => { + beforeAll(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("should open the certificate details page and return back", async () => { + await openValidEUCovidMessage(); + + await waitFor(element(by.text(I18n.t("global.buttons.details")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const detailsButton = element(by.text(I18n.t("global.buttons.details"))); + await detailsButton.tap(); + + await waitFor(element(by.text(I18n.t("global.buttons.close")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const closeButton = element(by.text(I18n.t("global.buttons.close"))); + await closeButton.tap(); + }); +}); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid03.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid03.e2e.ts new file mode 100644 index 00000000000..493ec46b041 --- /dev/null +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid03.e2e.ts @@ -0,0 +1,48 @@ +import { device } from "detox"; +import I18n from "../../../i18n"; +import { e2eWaitRenderTimeout } from "../../../__e2e__/config"; +import { ensureLoggedIn } from "../../../__e2e__/utils"; +import { openValidEUCovidMessage } from "./utils"; + +describe("EuCovidCert Valid", () => { + beforeAll(async () => { + await device.launchApp({ newInstance: true }); + await ensureLoggedIn(); + }); + + it("should save the certificate in the gallery", async () => { + await openValidEUCovidMessage(); + + await waitFor(element(by.text(I18n.t("global.genericSave")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const saveButton = element(by.text(I18n.t("global.genericSave"))); + await saveButton.tap(); + + await waitFor( + element( + by.text( + I18n.t( + "features.euCovidCertificate.save.bottomSheet.saveAsImage.title" + ) + ) + ) + ) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + const saveIntoGalleryButton = element( + by.text( + I18n.t("features.euCovidCertificate.save.bottomSheet.saveAsImage.title") + ) + ); + await saveIntoGalleryButton.tap(); + + await waitFor( + element(by.text(I18n.t("features.euCovidCertificate.save.ok"))) + ) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + }); +}); diff --git a/ts/features/euCovidCert/__e2e__/utils.ts b/ts/features/euCovidCert/__e2e__/utils.ts new file mode 100644 index 00000000000..759fde03a09 --- /dev/null +++ b/ts/features/euCovidCert/__e2e__/utils.ts @@ -0,0 +1,20 @@ +const euCovidCertValidSubject = "🏥 EUCovidCert - valid"; + +export const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; +export const messageListTestId = "MessageList_inbox"; +export const qrCodeTestId = "QRCode"; +export const fullScreenQrCodeTestId = "fullScreenQRCode"; + +export const scrollToEUCovidMessage = async (messageSubject: string) => { + await waitFor(element(by.text(messageSubject))) + .toBeVisible() + .whileElement(by.id(messageListTestId)) + .scroll(350, "down"); +}; + +export const openValidEUCovidMessage = async () => { + await scrollToEUCovidMessage(euCovidCertValidSubject); + + const subject = element(by.text(euCovidCertValidSubject)); + await subject.tap(); +}; diff --git a/ts/features/euCovidCert/analytics/index.ts b/ts/features/euCovidCert/analytics/index.ts index 4fcf7efbc51..748c6a73eb6 100644 --- a/ts/features/euCovidCert/analytics/index.ts +++ b/ts/features/euCovidCert/analytics/index.ts @@ -1,4 +1,5 @@ import { getType } from "typesafe-actions"; +import { constVoid } from "fp-ts/lib/function"; import { euCovidCertificateEnabled } from "../../../config"; import { mixpanel } from "../../../mixpanel"; import { Action } from "../../../store/actions/types"; @@ -11,7 +12,7 @@ import { const trackEuCovidCertificateActions = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(euCovidCertificateGet.request): return mp.track(action.type); @@ -25,7 +26,6 @@ const trackEuCovidCertificateActions = reason: getNetworkErrorMessage(action.payload) }); } - return Promise.resolve(); }; const trackEuCovidCertificateGetSuccessResponse = ( @@ -55,7 +55,7 @@ const trackEuCovidCertificateGetSuccessResponse = ( }; const emptyTracking = (_: NonNullable) => (__: Action) => - Promise.resolve(); + constVoid(); export default euCovidCertificateEnabled ? trackEuCovidCertificateActions diff --git a/ts/features/euCovidCert/components/MarkdownHandleCustomLink.tsx b/ts/features/euCovidCert/components/MarkdownHandleCustomLink.tsx index 789ee805216..5523fc8d95b 100644 --- a/ts/features/euCovidCert/components/MarkdownHandleCustomLink.tsx +++ b/ts/features/euCovidCert/components/MarkdownHandleCustomLink.tsx @@ -2,16 +2,16 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import { Toast } from "native-base"; import * as React from "react"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import { deriveCustomHandledLink } from "../../../components/ui/Markdown/handlers/link"; import I18n from "../../../i18n"; import { clipboardSetStringWithFeedback } from "../../../utils/clipboard"; import { taskLinking } from "../../../utils/url"; export const MarkdownHandleCustomLink = ( - props: React.ComponentProps + props: React.ComponentProps ): React.ReactElement => ( - { pipe( @@ -32,5 +32,5 @@ export const MarkdownHandleCustomLink = ( }} > {props.children} - + ); diff --git a/ts/features/euCovidCert/navigation/navigator.tsx b/ts/features/euCovidCert/navigation/navigator.tsx index 5c95a024313..4133fbbb5c3 100644 --- a/ts/features/euCovidCert/navigation/navigator.tsx +++ b/ts/features/euCovidCert/navigation/navigator.tsx @@ -12,8 +12,7 @@ const Stack = createStackNavigator(); export const EUCovidCertStackNavigator = () => ( void; -}>; - -const formattedTime = (time: number) => { - const date = new Date(time * 1000); - const minutes = date.getUTCMinutes(); - const seconds = date.getSeconds(); - return `${minutes.toString().padStart(2, "0")}:${seconds - .toString() - .padStart(2, "0")}`; -}; - -const ConuntDown = (props: Props) => { - const { totalSeconds: totalTime, onExpiration } = props; - const [remainingTime, setRemainingTime] = React.useState(totalTime); - - React.useEffect(() => { - const intervalId = setInterval(() => { - setRemainingTime(prevRemainingTime => - prevRemainingTime > 0 ? prevRemainingTime - 1 : 0 - ); - }, 1000); - - return () => { - clearInterval(intervalId); - }; - }, []); - - React.useEffect(() => { - if (remainingTime === 0) { - onExpiration(); - } - }, [onExpiration, remainingTime]); - - return ( - <> -

{`${formattedTime(remainingTime)}`}

- - ); -}; - -export default ConuntDown; diff --git a/ts/features/fastLogin/components/ModalHeader.tsx b/ts/features/fastLogin/components/ModalHeader.tsx deleted file mode 100644 index d4be6f18e04..00000000000 --- a/ts/features/fastLogin/components/ModalHeader.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from "react"; -import { View, StyleSheet } from "react-native"; -import { IOColors, Icon } from "@pagopa/io-app-design-system"; -import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; -import I18n from "../../../i18n"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { WithTestID } from "../../../types/WithTestID"; - -const styles = StyleSheet.create({ - mainContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - ...IOStyles.horizontalContentPadding, - paddingTop: 24, - backgroundColor: IOColors.white, - borderTopRightRadius: 16, - borderTopLeftRadius: 16 - }, - closeButton: { - paddingRight: 0, - justifyContent: "flex-end" - } -}); - -type ModalHeaderProps = WithTestID<{ - onClose: () => void; -}>; - -const ModalHeader = ({ onClose, testID }: ModalHeaderProps) => ( - - - - - - -); - -export default ModalHeader; diff --git a/ts/features/fastLogin/saga/pendingActionsSaga.ts b/ts/features/fastLogin/saga/pendingActionsSaga.ts index 290880e646b..903968ee1a3 100644 --- a/ts/features/fastLogin/saga/pendingActionsSaga.ts +++ b/ts/features/fastLogin/saga/pendingActionsSaga.ts @@ -1,9 +1,19 @@ import { SagaIterator } from "redux-saga"; -import { put, select, take, takeLatest } from "typed-redux-saga/macro"; +import { + put, + race, + select, + take, + takeLatest, + delay +} from "typed-redux-saga/macro"; +import { Millisecond } from "@pagopa/ts-commons/lib/units"; import { applicationInitialized } from "../../../store/actions/application"; import { clearPendingAction } from "../store/actions/tokenRefreshActions"; import { fastLoginPendingActionsSelector } from "../store/selectors"; +const ACTION_TO_WAIT_FOR_TIMEOUT = 3000 as Millisecond; + export function* watchPendingActionsSaga(): SagaIterator { yield* takeLatest(applicationInitialized, handleApplicationInitialized); } @@ -13,11 +23,24 @@ function* handleApplicationInitialized( ) { const { actionsToWaitFor } = _.payload; const pendingActions = yield* select(fastLoginPendingActionsSelector); + // if there are no pending actions, + // we don't need to wait for anything, + // because there is no saga to restart + // with the pending actions if (pendingActions.length === 0) { return; } + // If there are pending actions, + // we wait for the actionsToWaitFor, if any, + // and then we dispatch the pending actions. for (const action of actionsToWaitFor) { - yield* take(action); + // If for some reason the action is dispatched before + // we start waiting for it, we are stuck here. + // So we need to handle this race contion. + yield* race({ + take: take(action), + timeout: delay(ACTION_TO_WAIT_FOR_TIMEOUT) + }); } for (const action of pendingActions) { yield* put(action); diff --git a/ts/features/fastLogin/saga/tokenRefreshSaga.ts b/ts/features/fastLogin/saga/tokenRefreshSaga.ts index 9f960259544..4065f8ba3d0 100644 --- a/ts/features/fastLogin/saga/tokenRefreshSaga.ts +++ b/ts/features/fastLogin/saga/tokenRefreshSaga.ts @@ -44,6 +44,7 @@ import { import { getPin } from "../../../utils/keychain"; import { dismissSupport } from "../../../utils/supportAssistance"; import { MESSAGES_ROUTES } from "../../messages/navigation/routes"; +import ROUTES from "../../../navigation/routes"; export function* watchTokenRefreshSaga(): SagaIterator { yield* takeLatest(refreshSessionToken.request, handleRefreshSessionToken); @@ -90,7 +91,9 @@ function* handleRefreshSessionToken( } } else { // Lock the app - NavigationService.navigate(MESSAGES_ROUTES.MESSAGES_HOME); + NavigationService.navigate(ROUTES.MAIN, { + screen: MESSAGES_ROUTES.MESSAGES_HOME + }); yield* put(identificationRequest()); } } diff --git a/ts/features/fastLogin/saga/utils/index.ts b/ts/features/fastLogin/saga/utils/index.ts index cbbdb3df550..7562a148acb 100644 --- a/ts/features/fastLogin/saga/utils/index.ts +++ b/ts/features/fastLogin/saga/utils/index.ts @@ -11,13 +11,48 @@ import { savePendingAction } from "../../store/actions/tokenRefreshActions"; +type RefreshApiCallErrorHandlingTypeWithTypeField = { + errorMessage?: string; + skipThrowingError?: boolean; + // Because we check the "type" field in a type guard + // looking for a Redux Action, + // we must be sure to never put a property named "type" + // inside this object. + type?: never; +}; + +export type RefreshApiCallErrorHandlingType = Omit< + RefreshApiCallErrorHandlingTypeWithTypeField, + "type" +>; + +function isReduxAction( + obj?: A | RefreshApiCallErrorHandlingType +): obj is A { + return !!(obj && "type" in obj); +} + +// The function implementation with flexible parameter handling +// This has been done as a refactor to avoid the use of optional parameters export function* withRefreshApiCall( apiCall: Promise | R>>, - action?: A | undefined, - errorMessage?: string, - skipThrowingError: boolean = false + actionOrErrorHandling?: A | RefreshApiCallErrorHandlingType ): SagaIterator | R>> { const response = yield* call(() => apiCall); + + // eslint-disable-next-line functional/no-let + let action: A | undefined; + // eslint-disable-next-line functional/no-let + let errorHandling: typeof actionOrErrorHandling = {}; + + if (isReduxAction(actionOrErrorHandling)) { + action = actionOrErrorHandling; + } else { + errorHandling = + (actionOrErrorHandling as RefreshApiCallErrorHandlingType) ?? {}; + } + + const { errorMessage, skipThrowingError } = errorHandling; // BEWARE: we can cast to any only because we know for sure that f will // always return a Promise> if (E.isRight(response) && (response.right as any).status === 401) { diff --git a/ts/features/fastLogin/screens/AskUserInteractionScreen.tsx b/ts/features/fastLogin/screens/AskUserInteractionScreen.tsx new file mode 100644 index 00000000000..8656448388a --- /dev/null +++ b/ts/features/fastLogin/screens/AskUserInteractionScreen.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import { Modal } from "react-native"; +import { IOPictograms } from "@pagopa/io-app-design-system"; +import { useAvoidHardwareBackButton } from "../../../utils/useAvoidHardwareBackButton"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; + +type PrimaryActionType = Parameters< + typeof OperationResultScreenContent +>[0]["action"]; + +type SecondaryActionType = Parameters< + typeof OperationResultScreenContent +>[0]["secondaryAction"]; + +export type Props = { + title: string; + subtitle: string; + pictogramName: IOPictograms; + primaryAction?: PrimaryActionType; + secondaryAction?: SecondaryActionType; +}; + +const AskUserInteractionScreen = (props: Props) => { + useAvoidHardwareBackButton(); + + return ( + + + + ); +}; +export default AskUserInteractionScreen; diff --git a/ts/features/fastLogin/screens/AskUserInterarctionScreen.tsx b/ts/features/fastLogin/screens/AskUserInterarctionScreen.tsx deleted file mode 100644 index eff8a7e7e00..00000000000 --- a/ts/features/fastLogin/screens/AskUserInterarctionScreen.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import * as React from "react"; -import { - View, - SafeAreaView, - StyleSheet, - Modal, - GestureResponderEvent -} from "react-native"; -import { - ButtonOutline, - ButtonSolid, - HSpacer, - VSpacer, - IOPictograms, - Pictogram -} from "@pagopa/io-app-design-system"; -import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { H3 } from "../../../components/core/typography/H3"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import themeVariables from "../../../theme/variables"; -import { useAvoidHardwareBackButton } from "../../../utils/useAvoidHardwareBackButton"; -import CountDown from "../components/CountDown"; -import ModalHeader from "../components/ModalHeader"; - -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: themeVariables.contentPaddingLarge - }, - buttonContainer: { - flexDirection: "row", - padding: themeVariables.contentPadding - }, - title: { - textAlign: "center" - } -}); - -type ButtonStyle = { - type: "solid" | "outline"; - title: string; -}; -type ButtonStylesProps = { - submitButtonStyle: ButtonStyle; - cancelButtonStyle?: ButtonStyle; -}; -const DefaultButtonStylesProps: ButtonStylesProps = { - submitButtonStyle: { - type: "solid", - title: I18n.t("global.buttons.continue") - }, - cancelButtonStyle: { type: "outline", title: I18n.t("global.buttons.cancel") } -}; - -const DEFAULT_TIMER_DURATION = 60; - -export type Props = { - title: string; - subtitle: string; - pictogramName: IOPictograms; - onSubmit: () => void; - onCancel?: () => void; - onClose?: () => void; - onTimerExpired?: () => void; - timerDurationInSeconds?: number; - buttonStylesProps?: ButtonStylesProps; -}; - -const AskUserInteractionScreen = (props: Props) => { - useAvoidHardwareBackButton(); - - const { submitButtonStyle, cancelButtonStyle } = - props.buttonStylesProps || DefaultButtonStylesProps; - - const cancelButtonTitle = - cancelButtonStyle?.title || I18n.t("global.buttons.exit"); - const cancelButtonProps = { - fullWidth: true, - onPress: (_: GestureResponderEvent) => props.onCancel && props.onCancel(), - label: cancelButtonTitle, - accessibilityLabel: cancelButtonTitle - }; - - const submitButtonTitle = submitButtonStyle.title; - const submitButtonProps = { - fullWidth: true, - onPress: (_: GestureResponderEvent) => props.onSubmit(), - label: submitButtonTitle, - accessibilityLabel: submitButtonTitle - }; - - return ( - - - {props.onClose && ( - - )} - - - -

{props.title}

- - {props.subtitle} - - {props.onTimerExpired && ( - - )} -
- - {props.onCancel && ( - <> - - {cancelButtonStyle?.type === "outline" ? ( - - ) : ( - - )} - - - - )} - - {submitButtonStyle.type === "solid" ? ( - - ) : ( - - )} - - -
-
- ); -}; -export default AskUserInteractionScreen; diff --git a/ts/features/fastLogin/screens/FastLoginModals.tsx b/ts/features/fastLogin/screens/FastLoginModals.tsx index debd9d31b92..d2e077fbdac 100644 --- a/ts/features/fastLogin/screens/FastLoginModals.tsx +++ b/ts/features/fastLogin/screens/FastLoginModals.tsx @@ -3,12 +3,11 @@ import React from "react"; import { useDispatch } from "react-redux"; import { TokenRefreshState } from "../store/reducers/tokenRefreshReducer"; import { logoutRequest } from "../../../store/actions/authentication"; -import { openWebUrl } from "../../../utils/url"; import { askUserToRefreshSessionToken, clearTokenRefreshError } from "../store/actions/tokenRefreshActions"; -import AskUserInteractionScreen from "./AskUserInterarctionScreen"; +import AskUserInteractionScreen from "./AskUserInteractionScreen"; import RefreshTokenLoadingScreen from "./RefreshTokenLoadingScreen"; const FastLoginModals = ( @@ -20,21 +19,21 @@ const FastLoginModals = ( if (tokenRefreshing.kind === "no-pin-error") { return ( { - dispatch(clearTokenRefreshError()); - dispatch(logoutRequest()); - }} - buttonStylesProps={{ - submitButtonStyle: { - type: "solid", - title: I18n.t( - "fastLogin.userInteraction.sessionExpired.noPin.submitButtonTitle" - ) + primaryAction={{ + label: I18n.t( + "fastLogin.userInteraction.sessionExpired.noPin.submitButtonTitle" + ), + accessibilityLabel: I18n.t( + "fastLogin.userInteraction.sessionExpired.noPin.submitButtonTitle" + ), + onPress: () => { + dispatch(clearTokenRefreshError()); + dispatch(logoutRequest()); } }} /> @@ -44,26 +43,13 @@ const FastLoginModals = ( if (tokenRefreshing.kind === "transient-error") { return ( { - // FIXME: update this URL once available - // https://pagopa.atlassian.net/browse/IOPID-393 - openWebUrl("https://io.italia.it/faq"); - }} - buttonStylesProps={{ - submitButtonStyle: { - type: "solid", - title: I18n.t( - "fastLogin.userInteraction.sessionExpired.transientError.submitButtonTitle" - ) - } - }} /> ); } @@ -75,22 +61,22 @@ const FastLoginModals = ( if (isFastLoginUserInteractionNeeded) { return ( { - dispatch(askUserToRefreshSessionToken.success("yes")); - }} - buttonStylesProps={{ - submitButtonStyle: { - type: "solid", - title: I18n.t( - "fastLogin.userInteraction.sessionExpired.continueNavigation.submitButtonTitle" - ) + primaryAction={{ + label: I18n.t( + "fastLogin.userInteraction.sessionExpired.continueNavigation.submitButtonTitle" + ), + accessibilityLabel: I18n.t( + "fastLogin.userInteraction.sessionExpired.continueNavigation.submitButtonTitle" + ), + onPress: () => { + dispatch(askUserToRefreshSessionToken.success("yes")); } }} /> diff --git a/ts/features/fastLogin/screens/__tests__/AskUserToContinueScreen.test.tsx b/ts/features/fastLogin/screens/__tests__/AskUserToContinueScreen.test.tsx index 9a761083dd1..b90f48c68b6 100644 --- a/ts/features/fastLogin/screens/__tests__/AskUserToContinueScreen.test.tsx +++ b/ts/features/fastLogin/screens/__tests__/AskUserToContinueScreen.test.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { render, fireEvent, act } from "@testing-library/react-native"; +import { render, fireEvent } from "@testing-library/react-native"; import { Provider } from "react-redux"; import { Store, createStore } from "redux"; import * as _ from "lodash"; import type { IOPictograms } from "@pagopa/io-app-design-system"; -import AskUserInteractionScreen, { Props } from "../AskUserInterarctionScreen"; +import AskUserInteractionScreen, { Props } from "../AskUserInteractionScreen"; import { GlobalState } from "../../../../store/reducers/types"; import { appReducer } from "../../../../store/reducers"; import { applicationChangeState } from "../../../../store/actions/application"; @@ -14,87 +14,65 @@ jest.useFakeTimers(); const globalState = appReducer(undefined, applicationChangeState("active")); const store = createStore(appReducer, globalState as any); +const primaryActionButtonTitle = "Continue"; +const secondaryActionButtonTitle = "Cancel"; + const defaultProps = { title: "Test title", subtitle: "Test subtitle", pictogramName: "timeout" as IOPictograms, - onSubmit: jest.fn(), - onClose: jest.fn(), - onCancel: jest.fn(), - onTimerExpired: jest.fn(), - timerDurationInSeconds: 10 + primaryAction: { + label: primaryActionButtonTitle, + accessibilityLabel: primaryActionButtonTitle, + onPress: jest.fn() + }, + secondaryAction: { + label: secondaryActionButtonTitle, + accessibilityLabel: secondaryActionButtonTitle, + onPress: jest.fn() + } }; describe("AskUserInteractionScreen component", () => { it("should render properly", () => { - const { getByText, getByTestId } = renderComponent(defaultProps, store); + const { getByText } = renderComponent(defaultProps, store); expect(getByText(defaultProps.title)).toBeTruthy(); expect(getByText(defaultProps.subtitle)).toBeTruthy(); - const continueButton = getByText("Continue"); - const cancelButton = getByText("Cancel"); - const headerCloseButton = getByTestId("header-close-button"); - const countdownTimer = getByText("00:10"); + const continueButton = getByText(primaryActionButtonTitle); + const cancelButton = getByText(secondaryActionButtonTitle); expect(continueButton).toBeTruthy(); expect(cancelButton).toBeTruthy(); - expect(headerCloseButton).toBeTruthy(); - expect(countdownTimer).toBeTruthy(); - expect(getByTestId("countdown-timer")).toBeTruthy(); }); - it("should call onSubmit when the continue button is pressed", () => { + it("should call primaryAction onPress when the primary button is pressed", () => { const { getByText } = renderComponent(defaultProps, store); - const button = getByText("Continue"); + const button = getByText(primaryActionButtonTitle); fireEvent.press(button); - expect(defaultProps.onSubmit).toHaveBeenCalled(); + expect(defaultProps.primaryAction.onPress).toHaveBeenCalled(); }); - it("should call onCancel when the exit button is pressed", () => { + it("should call secondaryAction onPress when the secondary button is pressed", () => { const { getByText } = renderComponent(defaultProps, store); - const button = getByText("Cancel"); + const button = getByText(secondaryActionButtonTitle); fireEvent.press(button); - expect(defaultProps.onCancel).toHaveBeenCalled(); + expect(defaultProps.secondaryAction.onPress).toHaveBeenCalled(); }); - it("should call onClose when the close button is pressed", () => { - const { getByTestId } = renderComponent(defaultProps, store); - const button = getByTestId("header-close-button"); - fireEvent.press(button); - expect(defaultProps.onClose).toHaveBeenCalled(); - }); - - it("does not render exit button if onExit prop is not provided", () => { + it("does not render primary button if primaryAction is not provided", () => { const { queryByText } = renderComponent( - _.omit(defaultProps, "onCancel"), - store - ); - expect(queryByText("Cancel")).toBeNull(); - }); - - it("does not render close button if onExit prop is not provided", () => { - const { queryByTestId } = renderComponent( - _.omit(defaultProps, "onClose"), + _.omit(defaultProps, "primaryAction"), store ); - expect(queryByTestId("header-close-button")).toBeNull(); + expect(queryByText(primaryActionButtonTitle)).toBeNull(); }); - it("does not render timer if onTimerExpired prop is not provided", () => { - const { queryByTestId } = renderComponent( - _.omit(defaultProps, "onTimerExpired"), + it("does not render secondary button if secondaryAction is not provided", () => { + const { queryByText } = renderComponent( + _.omit(defaultProps, "secondaryAction"), store ); - expect(queryByTestId("countdown-timer")).toBeNull(); - }); - - it("should call onTimerExpired when the timer expires", async () => { - renderComponent(defaultProps, store); - - await act(() => { - jest.advanceTimersByTime(10 * 1000); - }); - - expect(defaultProps.onTimerExpired).toHaveBeenCalled(); + expect(queryByText(secondaryActionButtonTitle)).toBeNull(); }); }); diff --git a/ts/features/fastLogin/store/selectors/__tests__/emailUniquenessValidation.test.ts b/ts/features/fastLogin/store/selectors/__tests__/emailUniquenessValidation.test.ts deleted file mode 100644 index f47468e239e..00000000000 --- a/ts/features/fastLogin/store/selectors/__tests__/emailUniquenessValidation.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { Tuple2, ITuple2 } from "@pagopa/ts-commons/lib/tuples"; -import { BackendStatus } from "../../../../../../definitions/content/BackendStatus"; -import { baseRawBackendStatus } from "../../../../../store/reducers/__mock__/backendStatus"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { isEmailUniquenessValidationEnabledSelector } from ".."; -import { EmailUniquenessConfig } from "../../../../../../definitions/content/EmailUniquenessConfig"; - -jest.mock("react-native-device-info", () => ({ - getReadableVersion: jest.fn().mockReturnValue("1.2.3.4"), - getVersion: jest.fn().mockReturnValue("1.2.3.4") -})); - -jest.mock("../../../../../config", () => ({ - isNewCduFlow: true -})); - -describe('backend service Feature Flag "emailUniquenessValidation"', () => { - const status: BackendStatus = { - ...baseRawBackendStatus - }; - - const customStore = (emailUniquenessValidation: EmailUniquenessConfig) => - ({ - features: { - loginFeatures: { - fastLogin: { - optIn: { - enabled: true - } - } - } - }, - backendStatus: { - status: O.some({ - ...status, - config: { - ...status.config, - emailUniquenessValidation - } - }) - } - } as unknown as GlobalState); - - [ - Tuple2("0", false), - Tuple2("0.0", false), - Tuple2("0.0.0", false), - Tuple2("0.0.0.0", false), - Tuple2("1", true), - Tuple2("1.2", true), - Tuple2("1.2.3", true), - Tuple2("1.2.3.0", true), - Tuple2("1.2.3.1", true), - Tuple2("1.2.3.2", true), - Tuple2("1.2.3.3", true), - Tuple2("1.2.3.4", true), - Tuple2("1.2.3.5", false), - Tuple2("-1", false), - Tuple2("", false), - Tuple2(undefined, false), - Tuple2("?$&&/!@", false), - Tuple2("2", false), - Tuple2("1.3", false), - Tuple2("1.2.4", false) - ].forEach((t: ITuple2) => { - const [minAppVersion, expectedValue] = [t.e1, t.e2]; - it(`should return ${expectedValue} for ${JSON.stringify( - minAppVersion - )}`, () => { - const store = customStore({ - min_app_version: { - ios: minAppVersion, - android: minAppVersion - } - }); - - expect(isEmailUniquenessValidationEnabledSelector(store)).toBe( - expectedValue - ); - }); - }); -}); diff --git a/ts/features/fastLogin/store/selectors/index.ts b/ts/features/fastLogin/store/selectors/index.ts index e9f7b1215e2..7becc55e6b3 100644 --- a/ts/features/fastLogin/store/selectors/index.ts +++ b/ts/features/fastLogin/store/selectors/index.ts @@ -1,24 +1,10 @@ import { createSelector } from "reselect"; import { uniqWith, isEqual } from "lodash"; import { backendStatusSelector } from "../../../../store/reducers/backendStatus"; -import { - fastLoginOptIn, - fastLoginEnabled, - isNewCduFlow -} from "../../../../config"; +import { fastLoginOptIn, fastLoginEnabled } from "../../../../config"; import { GlobalState } from "../../../../store/reducers/types"; import { isPropertyWithMinAppVersionEnabled } from "../../../../store/reducers/featureFlagWithMinAppVersionStatus"; -export const isEmailUniquenessValidationEnabledSelector = createSelector( - backendStatusSelector, - backendStatus => - isPropertyWithMinAppVersionEnabled({ - backendStatus, - mainLocalFlag: isNewCduFlow, - configPropertyName: "emailUniquenessValidation" - }) -); - export const fastLoginOptInSelector = (state: GlobalState) => state.features.loginFeatures.fastLogin.optIn; diff --git a/ts/features/fci/analytics/index.ts b/ts/features/fci/analytics/index.ts index 0eaf9c86e46..99a6073ccd4 100644 --- a/ts/features/fci/analytics/index.ts +++ b/ts/features/fci/analytics/index.ts @@ -20,7 +20,7 @@ export const trackFciDocOpening = ( total_doc_count: number, environment: string ) => - void mixpanelTrack( + mixpanelTrack( "FCI_DOC_OPENING", buildEventProperties("UX", "action", { expire_date, @@ -34,7 +34,7 @@ export const trackFciUserExit = ( environment: string, cta_id?: string ) => - void mixpanelTrack( + mixpanelTrack( "FCI_USER_EXIT", buildEventProperties("UX", "exit", { screen_name, @@ -44,7 +44,7 @@ export const trackFciUserExit = ( ); export const trackFciUxConversion = (environment: string) => - void mixpanelTrack( + mixpanelTrack( "FCI_UX_CONVERSION", buildEventProperties("UX", "action", { environment @@ -52,7 +52,7 @@ export const trackFciUxConversion = (environment: string) => ); export const trackFciUserDataConfirmed = (environment: string) => - void mixpanelTrack( + mixpanelTrack( "FCI_USER_DATA_CONFIRMED", buildEventProperties("UX", "action", { environment }) ); @@ -63,7 +63,7 @@ export const trackFciDocOpeningSuccess = ( optional_sign_count: number, environment: string ) => - void mixpanelTrack( + mixpanelTrack( "FCI_DOC_OPENING_SUCCESS", buildEventProperties("UX", "control", { doc_count, @@ -74,13 +74,13 @@ export const trackFciDocOpeningSuccess = ( ); export const trackFciSigningDoc = (environment: string) => - void mixpanelTrack( + mixpanelTrack( "FCI_SIGNING_DOC", buildEventProperties("UX", "action", { environment }) ); export const trackFciShowSignatureFields = (environment: string) => - void mixpanelTrack( + mixpanelTrack( "FCI_SHOW_SIGNATURE_FIELDS", buildEventProperties("UX", "micro_action", { environment }) ); @@ -91,7 +91,7 @@ export const trackFciUxSuccess = ( optional_signed_count: number, environment: string ) => - void mixpanelTrack( + mixpanelTrack( "FCI_UX_SUCCESS", buildEventProperties("UX", "screen_view", { doc_signed_count, @@ -102,14 +102,14 @@ export const trackFciUxSuccess = ( ); export const trackFciStartSignature = (environment: string) => - void mixpanelTrack( + mixpanelTrack( "FCI_START_SIGNATURE", buildEventProperties("UX", "action", { environment }) ); const trackFciAction = (mp: NonNullable, environment: string) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(fciStartRequest): case getType(fciSignatureRequestFromId.request): @@ -146,7 +146,6 @@ const trackFciAction = }) ); } - return Promise.resolve(); }; export default trackFciAction; diff --git a/ts/features/fci/components/DocumentViewer.tsx b/ts/features/fci/components/DocumentViewer.tsx index 14b23cdf843..aa9e0f8825b 100644 --- a/ts/features/fci/components/DocumentViewer.tsx +++ b/ts/features/fci/components/DocumentViewer.tsx @@ -1,17 +1,18 @@ import React, { useState } from "react"; import { StyleSheet } from "react-native"; -import { constNull } from "fp-ts/lib/function"; import * as pot from "@pagopa/ts-commons/lib/pot"; import ReactNativeBlobUtil from "react-native-blob-util"; import Pdf from "react-native-pdf"; import * as S from "fp-ts/lib/string"; -import { IOColors } from "@pagopa/io-app-design-system"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import { + ButtonSolidProps, + FooterWithButtons, + IOColors +} from "@pagopa/io-app-design-system"; import I18n from "../../../i18n"; import { isIos } from "../../../utils/platform"; import { share } from "../../../utils/share"; import { showToast } from "../../../utils/showToast"; -import { confirmButtonProps } from "../../../components/buttons/ButtonConfigurations"; import { FciDownloadPreviewDirectoryPath } from "../saga/networking/handleDownloadDocument"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { fciDownloadPreview } from "../store/actions"; @@ -19,7 +20,7 @@ import { fciDownloadPathSelector, fciDownloadPreviewSelector } from "../store/reducers/fciDownloadPreview"; -import { LoadingErrorComponent } from "../../../components/LoadingErrorComponent"; +import LoadingComponent from "./LoadingComponent"; const styles = StyleSheet.create({ pdf: { @@ -31,94 +32,99 @@ const styles = StyleSheet.create({ export const getFileNameFromUrl = (url: string) => url.substring(url.lastIndexOf("/") + 1).split("?")[0] + ".pdf"; -const renderFooter = (url: string, filePath: string) => - isIos ? ( +const renderFooter = (url: string, filePath: string) => { + const confirmButtonProps: ButtonSolidProps = { + onPress: () => ReactNativeBlobUtil.ios.presentOptionsMenu(filePath), + label: I18n.t("messagePDFPreview.open"), + accessibilityLabel: I18n.t("messagePDFPreview.open") + }; + + const shareButtonProps: ButtonSolidProps = { + onPress: () => { + share( + `file://${ + FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url) + }`, + undefined, + false + )().catch(_ => { + showToast(I18n.t("messagePDFPreview.errors.sharing")); + }); + }, + label: I18n.t("global.buttons.share"), + accessibilityLabel: I18n.t("global.buttons.share") + }; + + const saveButtonProps: ButtonSolidProps = { + onPress: () => { + ReactNativeBlobUtil.MediaCollection.copyToMediaStore( + { + name: getFileNameFromUrl(url), + parentFolder: "", + mimeType: "application/pdf" + }, + "Download", + FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url) + ) + .then(_ => { + showToast( + I18n.t("messagePDFPreview.savedAtLocation", { + name: "attachment.displayName" + }), + "success" + ); + }) + .catch(_ => { + showToast(I18n.t("messagePDFPreview.errors.saving")); + }); + }, + label: I18n.t("messagePDFPreview.save"), + accessibilityLabel: I18n.t("messagePDFPreview.save") + }; + + const openButtonProps: ButtonSolidProps = { + onPress: () => { + ReactNativeBlobUtil.android + .actionViewIntent( + FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url), + "application/pdf" + ) + .catch(_ => { + showToast(I18n.t("messagePDFPreview.errors.opening")); + }); + }, + label: I18n.t("messagePDFPreview.open"), + accessibilityLabel: I18n.t("messagePDFPreview.open") + }; + + return isIos ? ( { - ReactNativeBlobUtil.ios.presentOptionsMenu(filePath); - }, I18n.t("messagePDFPreview.open"))} + primary={{ type: "Solid", buttonProps: confirmButtonProps }} /> ) : ( { - share( - `file://${ - FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url) - }`, - undefined, - false - )().catch(_ => { - showToast(I18n.t("messagePDFPreview.errors.sharing")); - }); - }, - title: I18n.t("global.buttons.share") - }} - midButton={{ - bordered: true, - primary: false, - onPress: () => { - ReactNativeBlobUtil.MediaCollection.copyToMediaStore( - { - name: getFileNameFromUrl(url), - parentFolder: "", - mimeType: "application/pdf" - }, - "Download", - FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url) - ) - .then(_ => { - showToast( - I18n.t("messagePDFPreview.savedAtLocation", { - name: "attachment.displayName" - }), - "success" - ); - }) - .catch(_ => { - showToast(I18n.t("messagePDFPreview.errors.saving")); - }); - }, - title: I18n.t("messagePDFPreview.save") + primary={{ + type: "Solid", + buttonProps: shareButtonProps }} - rightButton={confirmButtonProps(() => { - ReactNativeBlobUtil.android - .actionViewIntent( - FciDownloadPreviewDirectoryPath + "/" + getFileNameFromUrl(url), - "application/pdf" - ) - .catch(_ => { - showToast(I18n.t("messagePDFPreview.errors.opening")); - }); - }, I18n.t("messagePDFPreview.open"))} + secondary={{ type: "Outline", buttonProps: saveButtonProps }} + third={{ type: "Outline", buttonProps: openButtonProps }} /> ); +}; type Props = { documentUrl: string; - enableAnnotationRendering?: boolean; onLoadComplete?: (totalPages: number) => void; onPageChanged?: (page: number) => void; onError: () => void; }; -const LoadingComponent = () => ( - -); - export const DocumentViewer = (props: Props): React.ReactElement => { const [isError, setIsError] = useState(false); const documentUrl = props.documentUrl; - const enableAnnotationRendering = props.enableAnnotationRendering; const dispatch = useIODispatch(); const fciDownloadSelector = useIOSelector(fciDownloadPreviewSelector); const fciDownloadPath = useIOSelector(fciDownloadPathSelector); @@ -128,7 +134,7 @@ export const DocumentViewer = (props: Props): React.ReactElement => { }, [documentUrl, dispatch]); if (pot.isLoading(fciDownloadSelector)) { - return ; + return ; } if (pot.isError(fciDownloadSelector) || isError) { @@ -148,7 +154,6 @@ export const DocumentViewer = (props: Props): React.ReactElement => { setIsError(true); }} enablePaging - enableAnnotationRendering={enableAnnotationRendering ? true : false} /> {renderFooter(documentUrl, fciDownloadPath)} diff --git a/ts/features/fci/components/DocumentWithSignature.tsx b/ts/features/fci/components/DocumentWithSignature.tsx index 4560d707252..58a5aedbe10 100644 --- a/ts/features/fci/components/DocumentWithSignature.tsx +++ b/ts/features/fci/components/DocumentWithSignature.tsx @@ -1,26 +1,31 @@ import * as React from "react"; import Pdf from "react-native-pdf"; -import { Body, Container, Left, Right } from "native-base"; import { constNull, pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { SafeAreaView, StyleSheet } from "react-native"; -import { IconButton, IOColors } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import { StyleSheet, View } from "react-native"; +import { + ButtonSolidProps, + FooterWithButtons, + H5, + HSpacer, + IconButton, + IOColors, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; +import { SafeAreaView } from "react-native-safe-area-context"; import I18n from "../../../i18n"; import { ExistingSignatureFieldAttrs } from "../../../../definitions/fci/ExistingSignatureFieldAttrs"; import { SignatureFieldToBeCreatedAttrs } from "../../../../definitions/fci/SignatureFieldToBeCreatedAttrs"; import { fciSignatureDetailDocumentsSelector } from "../store/reducers/fciSignatureRequest"; -import AppHeader from "../../../components/ui/AppHeader"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { WithTestID } from "../../../types/WithTestID"; -import { H5 } from "../../../components/core/typography/H5"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; import { fciDocumentSignatureFields } from "../store/actions"; import { fciSignatureFieldDrawingSelector } from "../store/reducers/fciSignatureFieldDrawing"; -import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay"; import DocumentsNavigationBar from "./DocumentsNavigationBar"; +import LoadingComponent from "./LoadingComponent"; export type SignatureFieldAttrType = | ExistingSignatureFieldAttrs @@ -37,23 +42,32 @@ const styles = StyleSheet.create({ pdf: { flex: 1, backgroundColor: IOColors.bluegrey + }, + header: { + alignItems: "center", + flexDirection: "row" + }, + headerTitle: { + flex: 1, + textAlign: "center" } }); const DocumentWithSignature = (props: Props) => { const pdfRef = React.useRef(null); const [totalPages, setTotalPages] = React.useState(0); - const [currentPage, setCurrentPage] = React.useState(0); + const [currentPage, setCurrentPage] = React.useState(1); const documents = useIOSelector(fciSignatureDetailDocumentsSelector); const parsedDocuments = useIOSelector(fciSignatureFieldDrawingSelector); const { attrs, currentDoc } = props; const dispatch = useIODispatch(); const onContinuePress = () => props.onClose(); - const continueButtonProps = { - block: true, - primary: true, + const continueButtonProps: ButtonSolidProps = { onPress: onContinuePress, - title: I18n.t("features.fci.documents.footer.backToSignFieldsList") + label: I18n.t("features.fci.documents.footer.backToSignFieldsList"), + accessibilityLabel: I18n.t( + "features.fci.documents.footer.backToSignFieldsList" + ) }; /** @@ -89,7 +103,7 @@ const DocumentWithSignature = (props: Props) => { source={{ uri: document }} - page={page + 1} + page={page} onLoadComplete={(numberOfPages, _) => { setTotalPages(numberOfPages); }} @@ -100,7 +114,6 @@ const DocumentWithSignature = (props: Props) => { onError={props.onError} onPressLink={constNull} enablePaging - enableAnnotationRendering={false} style={styles.pdf} /> ), @@ -139,12 +152,6 @@ const DocumentWithSignature = (props: Props) => { ); }; - /** - * Renders the loading spinner. - * @returns a loading spinner overlay - */ - const LoadingView = () => ; - /** * Callback to be used when the pdf cannot be loaded or the signature field cannot be drawn. * It returns an empty fragment and calls the `onError` callback. @@ -161,39 +168,40 @@ const DocumentWithSignature = (props: Props) => { () => pot.fold( parsedDocuments, - () => , - () => , - () => , + () => , + () => , + () => , () => , some => ( ), - () => , - () => , + () => , + () => , () => ), [ErrorView, RenderPdf, parsedDocuments] ); return ( - - - - -
- {I18n.t("messagePDFPreview.title")} -
- - - - -
+ + + +
+ {I18n.t("messagePDFPreview.title")} +
+ +
+ { currentPage, totalPages })} - iconLeftColor={currentPage === 1 ? "bluegreyLight" : "blue"} - iconRightColor={currentPage === totalPages ? "bluegreyLight" : "blue"} + iconLeftDisabled={currentPage === 1} + iconRightDisabled={currentPage === totalPages} onPrevious={onPrevious} onNext={onNext} disabled={false} testID={"FciDocumentsNavBarTestID"} /> - - - - -
+ + + ); }; export default DocumentWithSignature; diff --git a/ts/features/fci/components/DocumentsNavigationBar.tsx b/ts/features/fci/components/DocumentsNavigationBar.tsx index 66c8bd7150e..cf7bdcb7c92 100644 --- a/ts/features/fci/components/DocumentsNavigationBar.tsx +++ b/ts/features/fci/components/DocumentsNavigationBar.tsx @@ -1,10 +1,13 @@ import * as React from "react"; import { View, StyleSheet } from "react-native"; -import { IOColors, Icon, HSpacer } from "@pagopa/io-app-design-system"; -import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; -import { H4 } from "../../../components/core/typography/H4"; -import { WithTestID } from "../../../types/WithTestID"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; +import { + IOColors, + HSpacer, + H6, + IOStyles, + IconButton, + WithTestID +} from "@pagopa/io-app-design-system"; const styles = StyleSheet.create({ container: { @@ -13,13 +16,7 @@ const styles = StyleSheet.create({ borderColor: IOColors.bluegreyLight, alignItems: "center", paddingTop: 12, - paddingBottom: 12 - }, - button: { - paddingLeft: 8, - paddingRight: 8, - paddingBottom: 0, - paddingTop: 0 + paddingBottom: 14 }, shadow: { // iOS @@ -43,8 +40,8 @@ export type IndicatorPositionEnum = "left" | "right"; type Props = WithTestID<{ titleRight: string; titleLeft: string; - iconRightColor?: IOColors; - iconLeftColor?: IOColors; + iconRightDisabled?: boolean; + iconLeftDisabled?: boolean; disabled?: boolean; indicatorPosition: IndicatorPositionEnum; onPrevious: () => void; @@ -52,41 +49,29 @@ type Props = WithTestID<{ }>; const renderNavigationComponent = ( - { onPrevious, onNext, disabled, iconLeftColor, iconRightColor }: Props, + { onPrevious, onNext, iconLeftDisabled, iconRightDisabled }: Props, title: string ) => ( <> {/* button left */} - - - -

{title}

+ icon="chevronLeft" + iconSize={24} + accessibilityLabel="previous" + /> +
{title}
{/* button right */} - - - + icon="chevronRight" + iconSize={24} + accessibilityLabel="next" + /> ); @@ -101,13 +86,13 @@ const DocumentsNavigationBar = (props: Props) => ( <> {renderNavigationComponent(props, props.titleLeft)} -

{props.titleRight}

+
{props.titleRight}
)} {props.indicatorPosition === "right" && ( <> -

{props.titleLeft}

+
{props.titleLeft}
{renderNavigationComponent(props, props.titleRight)} diff --git a/ts/features/fci/components/ErrorComponent.tsx b/ts/features/fci/components/ErrorComponent.tsx index b73a2f6f181..b74d06224a7 100644 --- a/ts/features/fci/components/ErrorComponent.tsx +++ b/ts/features/fci/components/ErrorComponent.tsx @@ -1,12 +1,22 @@ import * as React from "react"; -import { SafeAreaView } from "react-native"; +import { View } from "react-native"; import { EmailString } from "@pagopa/ts-commons/lib/strings"; -import { IOPictograms, Pictogram } from "@pagopa/io-app-design-system"; +import { + ButtonOutline, + ButtonSolid, + ButtonSolidProps, + IOPictograms, + IOSpacingScale, + IOStyles, + Pictogram, + VSpacer +} from "@pagopa/io-app-design-system"; +import { + SafeAreaView, + useSafeAreaInsets +} from "react-native-safe-area-context"; import I18n from "../../../i18n"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import { WithTestID } from "../../../types/WithTestID"; -import { FooterStackButton } from "../../../components/buttons/FooterStackButtons"; import { addTicketCustomField, assistanceToolRemoteConfig, @@ -35,11 +45,14 @@ export type Props = WithTestID<{ onPress: () => void; }>; +const DEFAULT_BOTTOM_PADDING: IOSpacingScale = 20; + const ErrorComponent = (props: Props) => { const dispatch = useIODispatch(); const signatureRequestId = useIOSelector(fciSignatureRequestIdSelector); const assistanceToolConfig = useIOSelector(assistanceToolConfigSelector); const choosenTool = assistanceToolRemoteConfig(assistanceToolConfig); + const insets = useSafeAreaInsets(); const zendeskAssistanceLogAndStart = () => { resetCustomFields(); @@ -64,63 +77,92 @@ const ErrorComponent = (props: Props) => { } }; - const retryButtonProps = { + const retryButtonProps: ButtonSolidProps = { testID: "FciRetryButtonTestID", - block: true, - primary: true, onPress: props.onPress, - title: I18n.t("features.fci.errors.buttons.retry") + fullWidth: true, + label: I18n.t("features.fci.errors.buttons.retry"), + accessibilityLabel: I18n.t("features.fci.errors.buttons.retry") }; - const closeButtonProps = { + const closeButtonProps: ButtonSolidProps = { testID: "FciCloseButtonTestID", - bordered: true, - block: true, onPress: props.onPress, - title: I18n.t("features.fci.errors.buttons.close") + fullWidth: true, + label: I18n.t("features.fci.errors.buttons.close"), + accessibilityLabel: I18n.t("features.fci.errors.buttons.close") }; - const assistanceButtonProps = { + const assistanceButtonProps: ButtonSolidProps = { testID: "FciAssistanceButtonTestID", - bordered: true, - primary: false, - block: true, + fullWidth: true, onPress: handleAskAssistance, - title: I18n.t("features.fci.errors.buttons.assistance") + label: I18n.t("features.fci.errors.buttons.assistance"), + accessibilityLabel: I18n.t("features.fci.errors.buttons.assistance") }; + /** + * Render the footer buttons as vertical stacked buttons + * @returns {React.ReactElement} + */ const footerButtons = () => { if (props.retry && props.assistance) { - return [retryButtonProps, assistanceButtonProps]; + return ( + <> + + + + + ); } if (props.retry) { - return [retryButtonProps, closeButtonProps]; + return ( + <> + + + + + ); } if (props.assistance) { - return [ - { - ...closeButtonProps, - bordered: false, - title: I18n.t("features.fci.errors.buttons.back") - }, - assistanceButtonProps - ]; + return ( + <> + + + + + ); } - return [closeButtonProps]; + return ; }; return ( - - - } - title={props.title} - body={props.subTitle} - email={props.email} - /> - - - + + } + title={props.title} + body={props.subTitle} + email={props.email} + /> + + {footerButtons()} + + ); }; diff --git a/ts/features/fci/components/InfoScreenComponent.tsx b/ts/features/fci/components/InfoScreenComponent.tsx index 74ad818934e..eee25b6785e 100644 --- a/ts/features/fci/components/InfoScreenComponent.tsx +++ b/ts/features/fci/components/InfoScreenComponent.tsx @@ -1,13 +1,10 @@ import * as React from "react"; import { Linking, StyleSheet, Text, View } from "react-native"; import { EmailString } from "@pagopa/ts-commons/lib/strings"; -import { VSpacer } from "@pagopa/io-app-design-system"; +import { Body, H2, LabelLink, VSpacer } from "@pagopa/io-app-design-system"; import { useFocusEffect } from "@react-navigation/native"; import themeVariables from "../../../theme/variables"; import { setAccessibilityFocus } from "../../../utils/accessibility"; -import { Body } from "../../../components/core/typography/Body"; -import { H2 } from "../../../components/core/typography/H2"; -import { Link } from "../../../components/core/typography/Link"; type Props = { image: React.ReactNode; @@ -38,9 +35,9 @@ const renderNode = (body: string | React.ReactNode, email?: EmailString) => { {email && <> } {email && ( - Linking.openURL(`mailto:${email}`)}> + Linking.openURL(`mailto:${email}`)}> {email} - + )} ); diff --git a/ts/features/fci/components/LinkedText.tsx b/ts/features/fci/components/LinkedText.tsx index 959b93acb0e..dcdcc524548 100644 --- a/ts/features/fci/components/LinkedText.tsx +++ b/ts/features/fci/components/LinkedText.tsx @@ -1,8 +1,6 @@ import * as React from "react"; import * as O from "fp-ts/lib/Option"; -import { H4 } from "../../../components/core/typography/H4"; -import { Link } from "../../../components/core/typography/Link"; -import { WithTestID } from "../../../types/WithTestID"; +import { H4, H6, LabelLink, WithTestID } from "@pagopa/io-app-design-system"; type Props = WithTestID<{ text: string; @@ -65,14 +63,14 @@ const LinkedText = (props: Props) => { const textToBeLinked = splitted[1]; const url = splitted[2]; return ( - onPress(getOrReplaceTagWithLink(url, props.replacementUrl)) } > {textToBeLinked} - + ); }; @@ -91,15 +89,15 @@ const LinkedText = (props: Props) => { {textWithSeparator.split("$@").map((text, index) => O.isSome(O.fromNullable(arrayOfLinkedText[index])) ? ( <> -

+

{text} -
+ {arrayOfLinkedText[index]} ) : ( -

+

{text} -
+ ) )} diff --git a/ts/features/fci/components/LoadingComponent.tsx b/ts/features/fci/components/LoadingComponent.tsx new file mode 100644 index 00000000000..373b8d29064 --- /dev/null +++ b/ts/features/fci/components/LoadingComponent.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { SafeAreaView, StyleSheet } from "react-native"; +import { Body, H3, VSpacer, WithTestID } from "@pagopa/io-app-design-system"; +import { LoadingIndicator } from "../../../components/ui/LoadingIndicator"; + +const styles = StyleSheet.create({ + main: { + flex: 1, + alignItems: "center", + justifyContent: "center" + }, + textAlignCenter: { + textAlign: "center" + } +}); + +type Props = WithTestID< + Readonly<{ + captionTitle?: string; + captionSubtitle?: string; + }> +>; + +/** + * A Component to display an animated spinner. + * It can be used to display a loading spinner with optionally a caption title and subtitle. + */ +const LoadingComponent = (props: Props) => { + const { captionTitle, captionSubtitle } = props; + + return ( + + + +

+ {captionTitle} +

+ + + {captionSubtitle} + +
+ ); +}; + +export default LoadingComponent; diff --git a/ts/features/fci/components/QtspClauseListItem.tsx b/ts/features/fci/components/QtspClauseListItem.tsx index 2a4d02cef90..13fd38a90c2 100644 --- a/ts/features/fci/components/QtspClauseListItem.tsx +++ b/ts/features/fci/components/QtspClauseListItem.tsx @@ -1,12 +1,9 @@ import * as React from "react"; import { StyleSheet, View } from "react-native"; import { useSelector } from "react-redux"; -import { Icon } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import TouchableDefaultOpacity from "../../../components/TouchableDefaultOpacity"; +import { CheckboxLabel, IOStyles } from "@pagopa/io-app-design-system"; import { QtspClause } from "../../../../definitions/fci/QtspClause"; import { fciQtspFilledDocumentUrlSelector } from "../store/reducers/fciQtspFilledDocument"; -import I18n from "../../../i18n"; import LinkedText from "./LinkedText"; type Props = { @@ -39,37 +36,21 @@ const QtspClauseListItem = (props: Props) => { return ( - { - - } + - { + { onChange(!checked); setChecked(!checked); }} - > - - - - + /> ); }; diff --git a/ts/features/fci/components/SignatureFieldItem.tsx b/ts/features/fci/components/SignatureFieldItem.tsx index c7e8a1185db..79272905710 100644 --- a/ts/features/fci/components/SignatureFieldItem.tsx +++ b/ts/features/fci/components/SignatureFieldItem.tsx @@ -1,10 +1,11 @@ import * as React from "react"; import { View, StyleSheet } from "react-native"; -import { IOColors, Icon } from "@pagopa/io-app-design-system"; -import { H4 } from "../../../components/core/typography/H4"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import TouchableDefaultOpacity from "../../../components/TouchableDefaultOpacity"; -import { Link } from "../../../components/core/typography/Link"; +import { + IOColors, + IOStyles, + LabelLink, + ListItemCheckbox +} from "@pagopa/io-app-design-system"; import I18n from "../../../i18n"; type Props = { @@ -20,15 +21,13 @@ const styles = StyleSheet.create({ paddingTop: 16, paddingBottom: 8, marginBottom: 16, - flexDirection: "column", borderBottomColor: IOColors.greyLight, borderBottomWidth: 1 }, details: { paddingTop: 16, paddingBottom: 8 - }, - titleMargin: { marginRight: 22, flex: 1 } + } }); const SignatureFieldItem = (props: Props) => { @@ -40,41 +39,17 @@ const SignatureFieldItem = (props: Props) => { return ( - -

- {props.title} -

- { - onChange(!checked); - }} - disabled={props.disabled} - style={{ alignSelf: "center" }} - > - - -
+ { + onChange(!checked); + }} + accessibilityLabel={props.title} + /> - { onPress={props.onPressDetail} > {I18n.t("features.fci.signatureFields.showOnDocument")} - +
); diff --git a/ts/features/fci/components/SignatureRequestItem.tsx b/ts/features/fci/components/SignatureRequestItem.tsx index 5b01e01185e..d0ecdc51e17 100644 --- a/ts/features/fci/components/SignatureRequestItem.tsx +++ b/ts/features/fci/components/SignatureRequestItem.tsx @@ -1,13 +1,15 @@ import * as React from "react"; import { View, StyleSheet } from "react-native"; -import { HSpacer } from "@pagopa/io-app-design-system"; -import { H4 } from "../../../components/core/typography/H4"; +import { + Badge, + Divider, + H6, + HSpacer, + LabelSmall +} from "@pagopa/io-app-design-system"; import { SignatureRequestListView } from "../../../../definitions/fci/SignatureRequestListView"; import { SignatureRequestStatusEnum } from "../../../../definitions/fci/SignatureRequestStatus"; -import { IOBadge } from "../../../components/core/IOBadge"; -import { LabelSmall } from "../../../components/core/typography/LabelSmall"; import TouchableDefaultOpacity from "../../../components/TouchableDefaultOpacity"; -import ItemSeparatorComponent from "../../../components/ItemSeparatorComponent"; import I18n from "../../../i18n"; type Props = { @@ -29,20 +31,16 @@ const SignatureRequestItem = (props: Props) => { switch (item.status) { case SignatureRequestStatusEnum.WAIT_FOR_SIGNATURE: return ( - ); case SignatureRequestStatusEnum.SIGNED: return ( - @@ -50,10 +48,9 @@ const SignatureRequestItem = (props: Props) => { case SignatureRequestStatusEnum.WAIT_FOR_QTSP: default: return ( - @@ -68,7 +65,7 @@ const SignatureRequestItem = (props: Props) => { testID={"FciSignatureRequestOnPress"} > -

{item.dossier_title}

+
{item.dossier_title}
{I18n.t("features.fci.requests.itemSubtitle", { date: item.created_at.toLocaleDateString(), @@ -80,7 +77,7 @@ const SignatureRequestItem = (props: Props) => { {renderStatusLabel()} - +
); }; diff --git a/ts/features/fci/components/__tests__/DocumentsNavigationBar.test.tsx b/ts/features/fci/components/__tests__/DocumentsNavigationBar.test.tsx index 5aaad148a56..7f591ee7afa 100644 --- a/ts/features/fci/components/__tests__/DocumentsNavigationBar.test.tsx +++ b/ts/features/fci/components/__tests__/DocumentsNavigationBar.test.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { fireEvent, render } from "@testing-library/react-native"; -import type { IOColors } from "@pagopa/io-app-design-system"; import DocumentsNavigationBar, { IndicatorPositionEnum } from "../DocumentsNavigationBar"; @@ -8,8 +7,8 @@ import DocumentsNavigationBar, { type Props = { titleRight: string; titleLeft: string; - iconRightColor?: IOColors; - iconLeftColor?: IOColors; + iconRightDisabled?: boolean; + iconLeftDisabled?: boolean; disabled?: boolean; indicatorPosition: IndicatorPositionEnum; onPrevious: () => void; @@ -21,8 +20,6 @@ describe("Test DocumentsNavigationBar component", () => { const props: Props = { titleRight: "Pagina 1 di 2", titleLeft: "Documento 1 di 2", - iconRightColor: "blue", - iconLeftColor: "blue", indicatorPosition: "left", onPrevious: jest.fn(), onNext: jest.fn() @@ -100,7 +97,7 @@ describe("Test DocumentsNavigationBar component", () => { const props: Props = { titleRight: "", titleLeft: "", - disabled: true, + iconRightDisabled: true, indicatorPosition: "left", onPrevious: jest.fn(), onNext: onPress @@ -121,7 +118,7 @@ describe("Test DocumentsNavigationBar component", () => { const props: Props = { titleRight: "", titleLeft: "", - disabled: true, + iconLeftDisabled: true, indicatorPosition: "left", onPrevious: jest.fn(), onNext: onPress diff --git a/ts/features/fci/components/__tests__/LoadingComponent.test.tsx b/ts/features/fci/components/__tests__/LoadingComponent.test.tsx new file mode 100644 index 00000000000..117b6c6fba9 --- /dev/null +++ b/ts/features/fci/components/__tests__/LoadingComponent.test.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { render } from "@testing-library/react-native"; +import LoadingComponent from "../LoadingComponent"; + +type Props = { + captionTitle?: string; + captionSubtitle?: string; +}; + +describe("Test LoadingComponent component", () => { + it("should render a LoadingComponent component with props correctly", () => { + const props = { + captionTitle: "Loading", + captionSubtitle: "Please wait..." + }; + const component = renderComponent({ ...props }); + expect(component).toBeTruthy(); + expect(component).toMatchSnapshot(); + }); + it("should render a LoadingComponent component with the right caption title", () => { + const props = { + captionTitle: "Loading", + captionSubtitle: "Please wait..." + }; + const component = renderComponent({ ...props }); + expect(component).toBeTruthy(); + expect( + component.queryByTestId("LoadingSpinnerCaptionTitleID") + ).toBeTruthy(); + expect(component.queryByText("Loading")).toBeTruthy(); + }); + it("should render a LoadingComponent component with the right caption subTitle", () => { + const props = { + captionTitle: "Loading", + captionSubtitle: "Please wait..." + }; + const component = renderComponent({ ...props }); + expect(component).toBeTruthy(); + expect( + component.queryByTestId("LoadingSpinnerCaptionSubTitleID") + ).toBeTruthy(); + expect(component.queryByText("Please wait...")).toBeTruthy(); + }); +}); + +const renderComponent = (props: Props) => + render(); diff --git a/ts/features/fci/components/__tests__/QtspClauseListItem.test.tsx b/ts/features/fci/components/__tests__/QtspClauseListItem.test.tsx index c3df86f94d8..0e9250a186b 100644 --- a/ts/features/fci/components/__tests__/QtspClauseListItem.test.tsx +++ b/ts/features/fci/components/__tests__/QtspClauseListItem.test.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { Store } from "redux"; import { Provider } from "react-redux"; import configureMockStore from "redux-mock-store"; -import { fireEvent } from "@testing-library/react-native"; import { mockQtspClausesMetadata } from "../../types/__mocks__/QtspClausesMetadata.mock"; import { QtspClause } from "../../../../../definitions/fci/QtspClause"; import QtspClauseListItem from "../QtspClauseListItem"; @@ -39,6 +38,7 @@ describe("Test QtspClauseListItem component", () => { }; const component = renderComponent({ ...props }, store); expect(component).toBeTruthy(); + expect(component).toMatchSnapshot(); }); it("should render a QtspClauseListItem component with container", () => { const mockStore = configureMockStore(); @@ -84,11 +84,11 @@ describe("Test QtspClauseListItem component", () => { const component = renderComponent({ ...props }, store); expect(component).toBeTruthy(); expect( - component.getByTestId("QtspClauseListItemCheckboxTestID") + component.getByTestId("QtspClauseListItemContainerTestID") ).toBeTruthy(); - expect(component.queryAllByText("io-checkbox-on")).toBeTruthy(); + expect(component.queryAllByText("legCheckOn")).toBeTruthy(); }); - it("should render a QtspClauseListItem component with checkbox clickable", () => { + it("should render a QtspClauseListItem component with checkbox enabled", () => { const mockStore = configureMockStore(); const store: ReturnType = mockStore(globalState); @@ -101,14 +101,9 @@ describe("Test QtspClauseListItem component", () => { }; const component = renderComponent({ ...props }, store); expect(component).toBeTruthy(); - const rightButton = component.getByTestId("QtspClauseListItemButtonTestID"); - expect(rightButton).toBeTruthy(); - expect(rightButton).toBeEnabled(); - fireEvent.press(rightButton); - expect(component.queryAllByText("io-checkbox-on")).toBeTruthy(); - fireEvent.press(rightButton); - expect(component.queryAllByText("io-checkbox-off")).toBeTruthy(); - expect(onPress).toHaveBeenCalledTimes(2); + const checkbox = component.getByTestId("AnimatedCheckbox"); + expect(checkbox).toBeTruthy(); + expect(checkbox).toBeEnabled(); }); it("should render a QtspClauseListItem component with right text for clause", () => { const mockStore = configureMockStore(); diff --git a/ts/features/fci/components/__tests__/SignatureFieldItem.test.tsx b/ts/features/fci/components/__tests__/SignatureFieldItem.test.tsx index 90aaeeaac08..c763f296088 100644 --- a/ts/features/fci/components/__tests__/SignatureFieldItem.test.tsx +++ b/ts/features/fci/components/__tests__/SignatureFieldItem.test.tsx @@ -31,7 +31,7 @@ describe("Test SignatureFieldItem component", () => { expect(component).toBeTruthy(); expect(component.queryByText("Clause title 1")).toBeTruthy(); }); - it("should render a SignatureFieldItem component with checkbox unchecked", () => { + it("should render a SignatureFieldItem component with checkbox enabled", () => { const props = { title: "Clause title 1", onChange: jest.fn(), @@ -39,43 +39,27 @@ describe("Test SignatureFieldItem component", () => { }; const component = renderComponent({ ...props }); expect(component).toBeTruthy(); - expect( - component.getByTestId("SignatureFieldItemCheckboxTestID") - ).toBeTruthy(); - expect(component.queryAllByText("io-checkbox-off")).toBeTruthy(); + const listItemCheckbox = component.getByTestId("ListItemCheckbox"); + expect(listItemCheckbox).toBeTruthy(); + const checkbox = component.getByTestId("AnimatedCheckboxInput"); + expect(checkbox).toBeTruthy(); + expect(checkbox).toBeEnabled(); }); - it("should render a SignatureFieldItem component with checkbox checked", () => { + it("should render a SignatureFieldItem component with checkbox disabled", () => { const props = { title: "Clause title 1", - value: true, + value: false, + disabled: true, onChange: jest.fn(), onPressDetail: jest.fn() }; const component = renderComponent({ ...props }); expect(component).toBeTruthy(); - expect( - component.getByTestId("SignatureFieldItemCheckboxTestID") - ).toBeTruthy(); - expect(component.queryAllByText("io-checkbox-on")).toBeTruthy(); - }); - it("should render a SignatureFieldItem component with checkbox clickable", () => { - const onPress = jest.fn(); - const props = { - title: "Clause title 1", - value: true, - onChange: onPress, - onPressDetail: jest.fn() - }; - const component = renderComponent({ ...props }); - expect(component).toBeTruthy(); - const rightButton = component.getByTestId("SignatureFieldItemButtonTestID"); - expect(rightButton).toBeTruthy(); - expect(rightButton).toBeEnabled(); - fireEvent.press(rightButton); - expect(component.queryAllByText("io-checkbox-on")).toBeTruthy(); - fireEvent.press(rightButton); - expect(component.queryAllByText("io-checkbox-off")).toBeTruthy(); - expect(onPress).toHaveBeenCalledTimes(2); + const listItemCheckbox = component.getByTestId("ListItemCheckbox"); + expect(listItemCheckbox).toBeTruthy(); + const checkbox = component.getByTestId("AnimatedCheckboxInput"); + expect(checkbox).toBeTruthy(); + expect(checkbox).toBeDisabled(); }); it("should render a SignatureFieldItem component with right text for details link", () => { const props = { diff --git a/ts/features/fci/components/__tests__/__snapshots__/DocumentsNavigationBar.test.tsx.snap b/ts/features/fci/components/__tests__/__snapshots__/DocumentsNavigationBar.test.tsx.snap index abecc0e98b7..7f74d2bf287 100644 --- a/ts/features/fci/components/__tests__/__snapshots__/DocumentsNavigationBar.test.tsx.snap +++ b/ts/features/fci/components/__tests__/__snapshots__/DocumentsNavigationBar.test.tsx.snap @@ -22,255 +22,267 @@ exports[`Test DocumentsNavigationBar component should render a DocumentsNavigati "backgroundColor": "#FFFFFF", "borderColor": "#CCD4DC", "flexDirection": "row", - "paddingBottom": 12, + "paddingBottom": 14, "paddingTop": 12, }, ] } > - - - + - - + > + +
+ + Documento 1 di 2 - - - + - - + > + +
+ + Pagina 1 di 2 diff --git a/ts/features/fci/components/__tests__/__snapshots__/LinkedText.test.tsx.snap b/ts/features/fci/components/__tests__/__snapshots__/LinkedText.test.tsx.snap index 493e234a7e7..b2d98facc5c 100644 --- a/ts/features/fci/components/__tests__/__snapshots__/LinkedText.test.tsx.snap +++ b/ts/features/fci/components/__tests__/__snapshots__/LinkedText.test.tsx.snap @@ -2,45 +2,50 @@ exports[`Test LinkedText component should render a LinkedText component with props correctly 1`] = ` C l a u s e t i t l e 1 `; diff --git a/ts/features/fci/components/__tests__/__snapshots__/LoadingComponent.test.tsx.snap b/ts/features/fci/components/__tests__/__snapshots__/LoadingComponent.test.tsx.snap new file mode 100644 index 00000000000..167905cc47b --- /dev/null +++ b/ts/features/fci/components/__tests__/__snapshots__/LoadingComponent.test.tsx.snap @@ -0,0 +1,275 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test LoadingComponent component should render a LoadingComponent component with props correctly 1`] = ` + + + + + + + + + + + + + + + + + + + + + Loading + + + + Please wait... + + +`; diff --git a/ts/features/fci/components/__tests__/__snapshots__/QtspClauseListItem.test.tsx.snap b/ts/features/fci/components/__tests__/__snapshots__/QtspClauseListItem.test.tsx.snap new file mode 100644 index 00000000000..ba6928c4972 --- /dev/null +++ b/ts/features/fci/components/__tests__/__snapshots__/QtspClauseListItem.test.tsx.snap @@ -0,0 +1,653 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test QtspClauseListItem component should render a QtspClauseListItem component with props correctly 1`] = ` + + + + + + + + + + + + + + + FCI_QTSP_TOS + + + + + + + + + + + + + + + + + + + + + (1) Io sottoscritto/a dichiaro quanto indicato nel + + + QUADRO E - AUTOCERTIFICAZIONE E SOTTOSCRIZIONE DA PARTE DEL TITOLARE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/fci/components/__tests__/__snapshots__/SignatureFieldItem.test.tsx.snap b/ts/features/fci/components/__tests__/__snapshots__/SignatureFieldItem.test.tsx.snap index 1312c2f0e10..c545330a2c6 100644 --- a/ts/features/fci/components/__tests__/__snapshots__/SignatureFieldItem.test.tsx.snap +++ b/ts/features/fci/components/__tests__/__snapshots__/SignatureFieldItem.test.tsx.snap @@ -6,7 +6,6 @@ exports[`Test SignatureFieldItem component should render a SignatureFieldItem co Object { "borderBottomColor": "#E6E9F2", "borderBottomWidth": 1, - "flexDirection": "column", "marginBottom": 16, "paddingBottom": 8, "paddingTop": 16, @@ -14,126 +13,193 @@ exports[`Test SignatureFieldItem component should render a SignatureFieldItem co } > - - Clause title 1 - - - - - + + + Clause title 1 + + + - - + + + + + + + + { const route = useRoute(); const dossierTitle = useIOSelector(fciSignatureRequestDossierTitleSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); - const { present, bottomSheet, dismiss } = useLegacyIOBottomSheetModal( - - + const { isExperimental } = useIOExperimentalDesign(); + + /** + * Callback function to abort the signature flow. + */ + const abortSignatureFlow = () => { + trackFciUserExit(route.name, fciEnvironment); + dispatch(fciEndRequest()); + dismiss(); + }; + + const cancelButtonProps: ButtonSolidProps = { + testID: "FciStopAbortingSignatureTestID", + onPress: () => dismiss(), + label: I18n.t("features.fci.abort.confirm"), + accessibilityLabel: I18n.t("features.fci.abort.confirm") + }; + const continueButtonProps: ButtonSolidProps = { + onPress: () => abortSignatureFlow(), + color: "danger", + label: I18n.t("features.fci.abort.cancel"), + accessibilityLabel: I18n.t("features.fci.abort.cancel") + }; + + const { + present: presentBs, + bottomSheet, + dismiss + } = useIOBottomSheetModal({ + title: I18n.t("features.fci.abort.title"), + component: ( + {I18n.t("features.fci.abort.content", { dossierTitle })} - - , - -

- {I18n.t("features.fci.abort.title")} -

-
, - 280, - dismiss(), - title: I18n.t("features.fci.abort.confirm") - }} - rightButton={{ - ...errorButtonProps(() => { - trackFciUserExit(route.name, fciEnvironment); - dispatch(fciEndRequest()); - dismiss(); - }, I18n.t("features.fci.abort.cancel")), - onPressWithGestureHandler: true - }} - /> - ); + + ), + snapPoint: [280], + footer: ( + + + + ) + }); + + /** + * Show an alert to confirm the abort signature flow. + */ + const showAlert = () => { + Alert.alert(I18n.t("features.fci.abort.alert.title"), undefined, [ + { + text: I18n.t("features.fci.abort.alert.cancel"), + style: "cancel" + }, + { + text: I18n.t("features.fci.abort.alert.confirm"), + onPress: () => abortSignatureFlow() + } + ]); + }; + + /** + * Overrides the present function of the bottom sheet to show an alert instead if the experimental design is enabled. + * This allows us to use an alert without changing single components which use the hook. + * TODO: remove when the experimental design will be enabled by default (SFEQS-2090) + */ + const present = () => (isExperimental ? showAlert() : presentBs()); return { dismiss, diff --git a/ts/features/fci/hooks/useFciCheckService.tsx b/ts/features/fci/hooks/useFciCheckService.tsx index 0a17fab5a47..b4d2ca6f994 100644 --- a/ts/features/fci/hooks/useFciCheckService.tsx +++ b/ts/features/fci/hooks/useFciCheckService.tsx @@ -1,30 +1,22 @@ import * as React from "react"; -import { StyleSheet, View } from "react-native"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useLegacyIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { H3 } from "../../../components/core/typography/H3"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import { + Body, + ButtonSolidProps, + FooterWithButtons +} from "@pagopa/io-app-design-system"; import I18n from "../../../i18n"; -import customVariables from "../../../theme/variables"; -import { confirmButtonProps } from "../../../components/buttons/ButtonConfigurations"; -import { H4 } from "../../../components/core/typography/H4"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { fciStartSigningRequest } from "../store/actions"; -import { upsertServicePreference } from "../../../store/actions/services/servicePreference"; +import { upsertServicePreference } from "../../services/store/actions"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { isServicePreferenceResponseSuccess } from "../../../types/services/ServicePreferenceResponse"; -import { servicePreferenceSelector } from "../../../store/reducers/entities/services/servicePreference"; +import { isServicePreferenceResponseSuccess } from "../../services/types/ServicePreferenceResponse"; +import { servicePreferenceSelector } from "../../services/store/reducers/servicePreference"; import { fciMetadataServiceIdSelector } from "../store/reducers/fciMetadata"; import { trackFciUxConversion } from "../analytics"; +import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; import { fciEnvironmentSelector } from "../store/reducers/fciEnvironment"; -const styles = StyleSheet.create({ - verticalPad: { - paddingVertical: customVariables.spacerHeight - } -}); - /** * A hook that returns a function to present the abort signature flow bottom sheet */ @@ -34,50 +26,55 @@ export const useFciCheckService = () => { const servicePreference = useIOSelector(servicePreferenceSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); const servicePreferenceValue = pot.getOrElse(servicePreference, undefined); - const { present, bottomSheet, dismiss } = useLegacyIOBottomSheetModal( - -

{I18n.t("features.fci.checkService.content")}

-
, - -

- {I18n.t("features.fci.checkService.title")} -

-
, - 280, - { - dispatch(fciStartSigningRequest()); - dismiss(); - }, - title: I18n.t("features.fci.checkService.cancel") - }} - rightButton={{ - ...confirmButtonProps(() => { - if ( - fciServiceId && - servicePreferenceValue && - isServicePreferenceResponseSuccess(servicePreferenceValue) - ) { - const sp = { ...servicePreferenceValue.value, inbox: true }; - dispatch( - upsertServicePreference.request({ - id: fciServiceId as ServiceId, - ...sp - }) - ); - } - trackFciUxConversion(fciEnvironment); - dispatch(fciStartSigningRequest()); - dismiss(); - }, I18n.t("features.fci.checkService.confirm")), - onPressWithGestureHandler: true - }} - /> - ); + const cancelButtonProps: ButtonSolidProps = { + onPress: () => { + dispatch(fciStartSigningRequest()); + dismiss(); + }, + label: I18n.t("features.fci.checkService.cancel"), + accessibilityLabel: I18n.t("features.fci.checkService.cancel") + }; + const confirmButtonProps: ButtonSolidProps = { + onPress: () => { + if ( + fciServiceId && + servicePreferenceValue && + isServicePreferenceResponseSuccess(servicePreferenceValue) + ) { + const sp = { ...servicePreferenceValue.value, inbox: true }; + dispatch( + upsertServicePreference.request({ + id: fciServiceId as ServiceId, + ...sp + }) + ); + } + trackFciUxConversion(fciEnvironment); + dispatch(fciStartSigningRequest()); + dismiss(); + }, + label: I18n.t("features.fci.checkService.confirm"), + accessibilityLabel: I18n.t("features.fci.checkService.confirm") + }; + const { present, bottomSheet, dismiss } = useIOBottomSheetModal({ + component: ( + + {I18n.t("features.fci.checkService.content")} + + ), + title: I18n.t("features.fci.checkService.title"), + snapPoint: [320], + footer: ( + + ) + }); return { dismiss, diff --git a/ts/features/fci/hooks/useFciNoSignatureFields.tsx b/ts/features/fci/hooks/useFciNoSignatureFields.tsx index 50081e4afe8..894ac25a2b7 100644 --- a/ts/features/fci/hooks/useFciNoSignatureFields.tsx +++ b/ts/features/fci/hooks/useFciNoSignatureFields.tsx @@ -1,80 +1,79 @@ -import * as React from "react"; -import { StyleSheet, View } from "react-native"; -import { StackActions, useNavigation } from "@react-navigation/native"; +import { + Body, + ButtonSolidProps, + FooterWithButtons +} from "@pagopa/io-app-design-system"; +import { StackActions } from "@react-navigation/native"; import { increment } from "fp-ts/lib/function"; -import { useLegacyIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { H3 } from "../../../components/core/typography/H3"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import * as React from "react"; import I18n from "../../../i18n"; -import customVariables from "../../../theme/variables"; -import { confirmButtonProps } from "../../../components/buttons/ButtonConfigurations"; -import { H4 } from "../../../components/core/typography/H4"; -import { FCI_ROUTES } from "../navigation/routes"; -import { fciSignatureDetailDocumentsSelector } from "../store/reducers/fciSignatureRequest"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../store/hooks"; +import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; import { trackFciStartSignature } from "../analytics"; +import { FCI_ROUTES } from "../navigation/routes"; import { fciEnvironmentSelector } from "../store/reducers/fciEnvironment"; +import { fciSignatureDetailDocumentsSelector } from "../store/reducers/fciSignatureRequest"; type Props = { currentDoc: number; }; -const styles = StyleSheet.create({ - verticalPad: { - paddingVertical: customVariables.spacerHeight - } -}); - /** * A hook that returns a function to present the abort signature flow bottom sheet */ export const useFciNoSignatureFields = (props: Props) => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const documents = useIOSelector(fciSignatureDetailDocumentsSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); const { currentDoc } = props; - const { present, bottomSheet, dismiss } = useLegacyIOBottomSheetModal( - -

{I18n.t("features.fci.noFields.content")}

-
, - -

- {I18n.t("features.fci.noFields.title")} -

-
, - 280, - { - dismiss(); - }, - title: I18n.t("features.fci.noFields.leftButton") - }} - rightButton={{ - ...confirmButtonProps(() => { - dismiss(); - if (currentDoc < documents.length - 1) { - navigation.dispatch( - StackActions.push(FCI_ROUTES.DOCUMENTS, { - attrs: undefined, - currentDoc: increment(currentDoc) - }) - ); - } else { - trackFciStartSignature(fciEnvironment); - navigation.navigate(FCI_ROUTES.MAIN, { - screen: FCI_ROUTES.USER_DATA_SHARE - }); - } - }, I18n.t("features.fci.noFields.rightButton")), - onPressWithGestureHandler: true - }} - /> - ); + const readButtonProps: ButtonSolidProps = { + onPress: () => { + dismiss(); + }, + label: I18n.t("features.fci.noFields.leftButton"), + accessibilityLabel: I18n.t("features.fci.noFields.leftButton") + }; + const confirmButtonProps: ButtonSolidProps = { + onPress: () => { + dismiss(); + if (currentDoc < documents.length - 1) { + navigation.dispatch( + StackActions.push(FCI_ROUTES.DOCUMENTS, { + attrs: undefined, + currentDoc: increment(currentDoc) + }) + ); + } else { + trackFciStartSignature(fciEnvironment); + navigation.navigate(FCI_ROUTES.MAIN, { + screen: FCI_ROUTES.USER_DATA_SHARE + }); + } + }, + label: I18n.t("features.fci.noFields.rightButton"), + accessibilityLabel: I18n.t("features.fci.noFields.rightButton") + }; + const { present, bottomSheet, dismiss } = useIOBottomSheetModal({ + component: ( + {I18n.t("features.fci.noFields.content")} + ), + title: I18n.t("features.fci.noFields.title"), + snapPoint: [280], + footer: ( + + ) + }); return { dismiss, diff --git a/ts/features/fci/navigation/FciStackNavigator.tsx b/ts/features/fci/navigation/FciStackNavigator.tsx index a5a3dd107f7..d4e56846eab 100644 --- a/ts/features/fci/navigation/FciStackNavigator.tsx +++ b/ts/features/fci/navigation/FciStackNavigator.tsx @@ -1,21 +1,22 @@ -import * as React from "react"; import { PathConfigMap } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; +import * as React from "react"; +import { AppParamsList } from "../../../navigation/params/AppParamsList"; import { isGestureEnabled } from "../../../utils/navigation"; -import FciDocumentsScreen from "../screens/valid/FciDocumentsScreen"; import FciRouterScreen from "../screens/FciRouterScreen"; -import FciSignatureFieldsScreen from "../screens/valid/FciSignatureFieldsScreen"; import FciDataSharingScreen from "../screens/valid/FciDataSharingScreen"; -import FciQtspClausesScreen from "../screens/valid/FciQtspClausesScreen"; -import FciThankyouScreen from "../screens/valid/FciThankyouScreen"; import { FciDocumentPreviewScreen } from "../screens/valid/FciDocumentPreviewScreen"; +import FciDocumentsScreen from "../screens/valid/FciDocumentsScreen"; +import FciQtspClausesScreen from "../screens/valid/FciQtspClausesScreen"; +import FciSignatureFieldsScreen from "../screens/valid/FciSignatureFieldsScreen"; import FciSignatureRequestsScreen from "../screens/valid/FciSignatureRequestsScreen"; -import { FCI_ROUTES } from "./routes"; +import FciThankyouScreen from "../screens/valid/FciThankyouScreen"; import { FciParamsList } from "./params"; +import { FCI_ROUTES } from "./routes"; const Stack = createStackNavigator(); -export const fciLinkingOptions: PathConfigMap = { +export const fciLinkingOptions: PathConfigMap = { [FCI_ROUTES.MAIN]: { path: "fci", screens: { @@ -28,10 +29,15 @@ export const fciLinkingOptions: PathConfigMap = { export const FciStackNavigator = () => ( - + ( component={FciDataSharingScreen} /> - + { fciDocumentSignatureFields.success({ ...cachedState, drawnBase64: newState.drawnBase64, - signaturePage: newState.signaturePage + signaturePage: newState.signaturePage + 1 // pdf-lib pages are 0-based but react-native-pdf pages are 1-based }) ) .run(); @@ -83,7 +83,12 @@ describe("handleDrawSignatureBox", () => { } ] ]) - .put(fciDocumentSignatureFields.success(newState)) + .put( + fciDocumentSignatureFields.success({ + ...newState, + signaturePage: newState.signaturePage + 1 // pdf-lib pages are 0-based but react-native-pdf pages are 1-based + }) + ) .run(); }); diff --git a/ts/features/fci/saga/handleDrawSignatureBox.ts b/ts/features/fci/saga/handleDrawSignatureBox.ts index 2f8f70c8616..d77eb86c653 100644 --- a/ts/features/fci/saga/handleDrawSignatureBox.ts +++ b/ts/features/fci/saga/handleDrawSignatureBox.ts @@ -28,7 +28,8 @@ export function* handleDrawSignatureBox( yield* put( fciDocumentSignatureFields.success({ ...state.value, - ...res + drawnBase64: res.drawnBase64, + signaturePage: res.signaturePage + 1 // pdf-lib pages are 0-based but react-native-pdf pages are 1-based }) ); } else { @@ -42,7 +43,8 @@ export function* handleDrawSignatureBox( fciDocumentSignatureFields.success({ rawBase64, uri: action.payload.uri, - ...res + drawnBase64: res.drawnBase64, + signaturePage: res.signaturePage + 1 // pdf-lib pages are 0-based but react-native-pdf pages are 1-based }) ); } diff --git a/ts/features/fci/screens/FciRouterScreen.tsx b/ts/features/fci/screens/FciRouterScreen.tsx index 9d9bc6eadd8..5de3fe04fad 100644 --- a/ts/features/fci/screens/FciRouterScreen.tsx +++ b/ts/features/fci/screens/FciRouterScreen.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { constNull, pipe } from "fp-ts/lib/function"; +import { pipe } from "fp-ts/lib/function"; import * as pot from "@pagopa/ts-commons/lib/pot"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; @@ -11,7 +11,6 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { FciParamsList } from "../navigation/params"; import { fciEndRequest, fciSignatureRequestFromId } from "../store/actions"; import { fciSignatureRequestSelector } from "../store/reducers/fciSignatureRequest"; -import { LoadingErrorComponent } from "../../../components/LoadingErrorComponent"; import SuccessComponent from "../components/SuccessComponent"; import GenericErrorComponent from "../components/GenericErrorComponent"; import { withValidatedEmail } from "../../../components/helpers/withValidatedEmail"; @@ -24,6 +23,7 @@ import { } from "../../../utils/errors"; import { ProblemJson } from "../../../../definitions/fci/ProblemJson"; import ErrorComponent from "../components/ErrorComponent"; +import LoadingComponent from "../components/LoadingComponent"; export type FciRouterScreenNavigationParams = Readonly<{ signatureRequestId: SignatureRequestDetailView["id"]; @@ -57,13 +57,8 @@ const FciSignatureScreen = ( ); } - const LoadingComponent = () => ( - + const LoadingView = () => ( + ); const GenericError = (status?: ProblemJson["status"]) => { @@ -110,13 +105,13 @@ const FciSignatureScreen = ( return pot.fold( fciSignatureRequest, - () => , - () => , - () => , + () => , + () => , + () => , renderErrorComponent, b => , - () => , - () => , + () => , + () => , () => renderErrorComponent() ); }; diff --git a/ts/features/fci/screens/__tests__/FciDocumentsScreen.test.tsx b/ts/features/fci/screens/__tests__/FciDocumentsScreen.test.tsx index 7eb3cb9c989..4d9d53ca7d2 100644 --- a/ts/features/fci/screens/__tests__/FciDocumentsScreen.test.tsx +++ b/ts/features/fci/screens/__tests__/FciDocumentsScreen.test.tsx @@ -6,8 +6,11 @@ import { GlobalState } from "../../../../store/reducers/types"; import FciDocumentsScreen from "../valid/FciDocumentsScreen"; import { FCI_ROUTES } from "../../navigation/routes"; import { mockSignatureRequestDetailView } from "../../types/__mocks__/SignatureRequestDetailView.mock"; +import { + fciDownloadPreview, + fciSignatureRequestFromId +} from "../../store/actions"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; -import { fciSignatureRequestFromId } from "../../store/actions"; describe("Test FciDocuments screen", () => { beforeEach(() => { @@ -21,8 +24,7 @@ describe("Test FciDocuments screen", () => { ); store.dispatch( fciSignatureRequestFromId.success({ - ...mockSignatureRequestDetailView, - documents: [] + ...mockSignatureRequestDetailView }) ); const component = renderComponent(store); @@ -36,8 +38,12 @@ describe("Test FciDocuments screen", () => { ); store.dispatch( fciSignatureRequestFromId.success({ - ...mockSignatureRequestDetailView, - documents: [] + ...mockSignatureRequestDetailView + }) + ); + store.dispatch( + fciDownloadPreview.success({ + path: mockSignatureRequestDetailView.documents[0].url }) ); const testComponent = renderComponent(store); diff --git a/ts/features/fci/screens/__tests__/FciThankyouScreen.test.tsx b/ts/features/fci/screens/__tests__/FciThankyouScreen.test.tsx index 2ef8d0ec8bf..43dea643ecd 100644 --- a/ts/features/fci/screens/__tests__/FciThankyouScreen.test.tsx +++ b/ts/features/fci/screens/__tests__/FciThankyouScreen.test.tsx @@ -73,8 +73,7 @@ describe("Test FciThankyouScreen screen", () => { store.dispatch(fciSigningRequest.success(mockSignatureDetailView)); const component = renderComponent(store); expect(component).toBeTruthy(); - expect(component.queryByTestId("FciTypSuccessTestID")).toBeTruthy(); - const closeButton = component.getByTestId("FciTypSuccessFooterButton"); + const closeButton = component.getByTestId("FciTypCloseButton"); expect(closeButton).toBeTruthy(); expect(closeButton).toBeEnabled(); fireEvent.press(closeButton, "onPress"); diff --git a/ts/features/fci/screens/valid/FciDataSharingScreen.tsx b/ts/features/fci/screens/valid/FciDataSharingScreen.tsx index 7987743d860..810f38b60ef 100644 --- a/ts/features/fci/screens/valid/FciDataSharingScreen.tsx +++ b/ts/features/fci/screens/valid/FciDataSharingScreen.tsx @@ -1,16 +1,26 @@ +import { + Body, + ButtonSolidProps, + FooterWithButtons, + H2, + H6, + HSpacer, + IOStyles, + IconButton, + LabelLink, + ListItemNav, + VSpacer +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useNavigation, useRoute } from "@react-navigation/native"; -import { List } from "native-base"; -import * as React from "react"; -import { SafeAreaView, StyleSheet, View } from "react-native"; +import { useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; -import { Icon } from "@pagopa/io-app-design-system"; -import { H4 } from "../../../../components/core/typography/H4"; -import { Link } from "../../../../components/core/typography/Link"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import ListItemComponent from "../../../../components/screens/ListItemComponent"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; +import * as React from "react"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { withValidatedEmail } from "../../../../components/helpers/withValidatedEmail"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../navigation/routes"; import { useIOSelector } from "../../../../store/hooks"; import { profileEmailSelector, @@ -18,41 +28,17 @@ import { profileNameSelector, profileSelector } from "../../../../store/reducers/profile"; -import customVariables from "../../../../theme/variables"; +import { formatFiscalCodeBirthdayAsShortFormat } from "../../../../utils/dates"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { capitalize } from "../../../../utils/strings"; -import { - cancelButtonProps, - confirmButtonProps -} from "../../../../components/buttons/ButtonConfigurations"; -import { useFciAbortSignatureFlow } from "../../hooks/useFciAbortSignatureFlow"; -import ROUTES from "../../../../navigation/routes"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import { withValidatedEmail } from "../../../../components/helpers/withValidatedEmail"; -import ScreenContent from "../../../../components/screens/ScreenContent"; import { trackFciUserDataConfirmed, trackFciUserExit } from "../../analytics"; -import { formatFiscalCodeBirthdayAsShortFormat } from "../../../../utils/dates"; +import { useFciAbortSignatureFlow } from "../../hooks/useFciAbortSignatureFlow"; +import { FCI_ROUTES } from "../../navigation/routes"; import { fciEnvironmentSelector } from "../../store/reducers/fciEnvironment"; const styles = StyleSheet.create({ - padded: { - paddingLeft: customVariables.contentPadding, - paddingRight: customVariables.contentPadding, - paddingBottom: customVariables.contentPadding - }, - verticalPadding: { - paddingTop: customVariables.spacerHeight, - paddingBottom: customVariables.spacerHeight - }, - paddingTextLarge: { - paddingLeft: 14 - }, - paddingText: { - paddingLeft: 4 - }, alertTextContainer: { flexDirection: "row", - justifyContent: "space-between", alignItems: "center" } }); @@ -64,7 +50,7 @@ const FciDataSharingScreen = (): React.ReactElement => { const name = useIOSelector(profileNameSelector); const fiscalCode = useIOSelector(profileFiscalCodeSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); - const navigation = useNavigation(); + const navigation = useIONavigation(); const route = useRoute(); const familyName = pot.getOrElse( pot.map(profile, p => capitalize(p.family_name)), @@ -76,106 +62,146 @@ const FciDataSharingScreen = (): React.ReactElement => { ); const email = useIOSelector(profileEmailSelector); + useHeaderSecondLevel({ + title: I18n.t("features.fci.title"), + contextualHelp: emptyContextualHelp, + supportRequest: true + }); + const { present, bottomSheet: fciAbortSignature } = useFciAbortSignatureFlow(); + const cancelButtonProps: ButtonSolidProps = { + onPress: () => present(), + label: I18n.t("features.fci.shareDataScreen.cancel"), + accessibilityLabel: I18n.t("features.fci.shareDataScreen.cancel") + }; + + const confirmButtonProps: ButtonSolidProps = { + onPress: () => { + trackFciUserDataConfirmed(fciEnvironment); + navigation.navigate(FCI_ROUTES.MAIN, { + screen: FCI_ROUTES.QTSP_TOS + }); + }, + label: I18n.t("features.fci.shareDataScreen.confirm"), + accessibilityLabel: I18n.t("features.fci.shareDataScreen.confirm") + }; + const AlertTextComponent = () => ( - -

- {I18n.t("features.fci.shareDataScreen.alertText")} - - { - trackFciUserExit(route.name, fciEnvironment, "modifica_email"); - navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.INSERT_EMAIL_SCREEN - }); - }} - > - {I18n.t("features.fci.shareDataScreen.alertLink")} - -

+ undefined} + /> + + +
+ {I18n.t("features.fci.shareDataScreen.alertText")} + + { + trackFciUserExit(route.name, fciEnvironment, "modifica_email"); + navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.INSERT_EMAIL_SCREEN, + params: { + isOnboarding: false, + isFciEditEmailFlow: true + } + }); + }} + > + {I18n.t("features.fci.shareDataScreen.alertLink")} + +
+
); return ( - - - - - - {name && ( - - )} - {familyName && ( - - )} - {birthDate && ( - - )} - {fiscalCode && ( - - )} - {O.isSome(email) && ( - - )} - - - - - - present(), - I18n.t("features.fci.shareDataScreen.cancel") + <> + +

{I18n.t("features.fci.shareDataScreen.title")}

+ + {I18n.t("features.fci.shareDataScreen.content")} + {name && ( + undefined} + hideChevron + /> + )} + {familyName && ( + undefined} + hideChevron + accessibilityLabel={I18n.t( + "features.fci.shareDataScreen.familyName" + )} + /> + )} + {birthDate && ( + { - trackFciUserDataConfirmed(fciEnvironment); - navigation.navigate("FCI_QTSP_TOS"); - }, `${I18n.t("features.fci.shareDataScreen.confirm")}`)} + onPress={() => undefined} /> -
-
+ )} + {fiscalCode && ( + undefined} + /> + )} + {O.isSome(email) && ( + <> + undefined} + /> + + + )} +
+ + + {fciAbortSignature} - + ); }; diff --git a/ts/features/fci/screens/valid/FciDocumentPreviewScreen.tsx b/ts/features/fci/screens/valid/FciDocumentPreviewScreen.tsx index 348f6acb75c..7db7a4d1964 100644 --- a/ts/features/fci/screens/valid/FciDocumentPreviewScreen.tsx +++ b/ts/features/fci/screens/valid/FciDocumentPreviewScreen.tsx @@ -1,8 +1,5 @@ import React from "react"; -import { Platform, SafeAreaView, StyleSheet } from "react-native"; import * as S from "fp-ts/lib/string"; -import { IconButton } from "@pagopa/io-app-design-system"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; @@ -12,28 +9,27 @@ import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { fciDownloadPreviewClear, fciEndRequest } from "../../store/actions"; import { fciDownloadPathSelector } from "../../store/reducers/fciDownloadPreview"; import GenericErrorComponent from "../../components/GenericErrorComponent"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; export type FciDocumentPreviewScreenNavigationParams = Readonly<{ documentUrl: string; - enableAnnotationRendering?: boolean; }>; -const styles = StyleSheet.create({ - container: { - flex: 1 - } -}); - export const FciDocumentPreviewScreen = ( props: IOStackNavigationRouteProps ): React.ReactElement => { const [isError, setIsError] = React.useState(false); const documentUrl = props.route.params.documentUrl ?? ""; - const enableAnnotationRendering = - props.route.params.enableAnnotationRendering; const fciDownloadPath = useIOSelector(fciDownloadPathSelector); const dispatch = useIODispatch(); + useHeaderSecondLevel({ + title: I18n.t("messagePDFPreview.title"), + contextualHelp: emptyContextualHelp, + supportRequest: true, + goBack: () => dispatch(fciDownloadPreviewClear({ path: fciDownloadPath })) + }); + if (isError) { return ( { - dispatch(fciDownloadPreviewClear({ path: fciDownloadPath })); - }} - accessibilityLabel={I18n.t("global.buttons.back")} - /> - ); - return ( - - - {S.isEmpty(documentUrl) === false && ( - setIsError(true)} - /> - )} - - + <> + {S.isEmpty(documentUrl) === false && ( + setIsError(true)} + /> + )} + ); }; diff --git a/ts/features/fci/screens/valid/FciDocumentsScreen.tsx b/ts/features/fci/screens/valid/FciDocumentsScreen.tsx index a50dc51cbbb..09b109c8d1b 100644 --- a/ts/features/fci/screens/valid/FciDocumentsScreen.tsx +++ b/ts/features/fci/screens/valid/FciDocumentsScreen.tsx @@ -4,7 +4,7 @@ import { pipe } from "fp-ts/lib/function"; import * as RA from "fp-ts/lib/ReadonlyArray"; import * as O from "fp-ts/lib/Option"; import * as S from "fp-ts/lib/string"; -import { Platform, SafeAreaView, StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import { useSelector } from "react-redux"; import { RouteProp, @@ -13,10 +13,13 @@ import { useNavigation, useRoute } from "@react-navigation/native"; -import { IconButton, IOColors } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; +import { + BlockButtonProps, + ButtonSolidProps, + FooterWithButtons, + IOColors, + IOStyles +} from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import DocumentsNavigationBar from "../../components/DocumentsNavigationBar"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; @@ -34,7 +37,6 @@ import { import { fciDocumentSignaturesSelector } from "../../store/reducers/fciDocumentSignatures"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { fciDownloadPathSelector } from "../../store/reducers/fciDownloadPreview"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import { trackFciDocOpeningSuccess, trackFciSigningDoc } from "../../analytics"; import { getOptionalSignatureFields, @@ -43,6 +45,8 @@ import { } from "../../utils/signatureFields"; import { useFciNoSignatureFields } from "../../hooks/useFciNoSignatureFields"; import { fciEnvironmentSelector } from "../../store/reducers/fciEnvironment"; +import LoadingComponent from "../../components/LoadingComponent"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; const styles = StyleSheet.create({ pdf: { @@ -58,7 +62,7 @@ export type FciDocumentsScreenNavigationParams = Readonly<{ const FciDocumentsScreen = () => { const pdfRef = React.useRef(null); const [totalPages, setTotalPages] = React.useState(0); - const [currentPage, setCurrentPage] = React.useState(0); + const [currentPage, setCurrentPage] = React.useState(1); const route = useRoute>(); const currentDoc = route.params.currentDoc ?? 0; const documents = useSelector(fciSignatureDetailDocumentsSelector); @@ -131,19 +135,28 @@ const FciDocumentsScreen = () => { const onCancelPress = () => present(); - const cancelButtonProps = { - block: true, - light: false, - bordered: true, + const cancelButtonProps: ButtonSolidProps = { onPress: onCancelPress, - title: I18n.t("features.fci.documents.footer.cancel") + label: I18n.t("features.fci.documents.footer.cancel"), + accessibilityLabel: I18n.t("features.fci.documents.footer.cancel") }; - const continueButtonProps = { - block: true, - primary: true, + const continueButtonProps: ButtonSolidProps = { onPress: onContinuePress, - title: I18n.t("features.fci.documents.footer.continue") + label: I18n.t("features.fci.documents.footer.continue"), + accessibilityLabel: I18n.t("features.fci.documents.footer.continue") + }; + + const keepReadingButtonProps: ButtonSolidProps = { + onPress: () => pointToPage(totalPages), + label: I18n.t("global.buttons.continue"), + accessibilityLabel: I18n.t("global.buttons.continue") + }; + + const secondaryButtonProps: BlockButtonProps = { + type: currentPage < totalPages ? "Outline" : "Solid", + buttonProps: + currentPage < totalPages ? keepReadingButtonProps : continueButtonProps }; const pointToPage = (page: number) => @@ -153,14 +166,6 @@ const FciDocumentsScreen = () => { O.map(_ => _.setPage(page)) ); - const keepReadingButtonProps = { - block: true, - light: true, - bordered: true, - onPress: () => pointToPage(totalPages), - title: I18n.t("global.buttons.continue") - }; - const renderPager = () => ( { setCurrentPage(page); }} enablePaging - enableAnnotationRendering={false} style={styles.pdf} /> ); @@ -203,64 +207,56 @@ const FciDocumentsScreen = () => { ); }; - const customGoBack: React.ReactElement = ( - { - if (currentDoc <= 0) { - dispatch(fciClearStateRequest()); - } - navigation.goBack(); - }} - accessibilityLabel={I18n.t("global.buttons.back")} - /> - ); + useHeaderSecondLevel({ + title: I18n.t("features.fci.title"), + supportRequest: true, + contextualHelp: emptyContextualHelp, + goBack: () => { + if (currentDoc <= 0) { + dispatch(fciClearStateRequest()); + } + navigation.goBack(); + } + }); - const renderFooterButtons = () => - currentPage < totalPages ? keepReadingButtonProps : continueButtonProps; + if (S.isEmpty(downloadPath)) { + return ; + } return ( - - - - - {documents.length > 0 && ( - <> - {renderPager()} - - - )} - - {fciAbortSignature} - {fciNoSignatureFields} - - + <> + + + {documents.length > 0 && ( + <> + {renderPager()} + + + )} + + {fciAbortSignature} + {fciNoSignatureFields} + ); }; export default FciDocumentsScreen; diff --git a/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx b/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx index 2e4b72be5f0..6b4efd4465d 100644 --- a/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx +++ b/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx @@ -1,12 +1,16 @@ import * as React from "react"; -import { SafeAreaView, FlatList, View } from "react-native"; +import { FlatList, View, ScrollView } from "react-native"; import { useSelector } from "react-redux"; -import { useNavigation } from "@react-navigation/native"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { constNull } from "fp-ts/lib/function"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; +import { + Body, + ButtonSolidProps, + Divider, + FooterWithButtons, + H2, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { @@ -14,34 +18,33 @@ import { fciQtspPrivacyTextSelector, fciQtspPrivacyUrlSelector } from "../../store/reducers/fciQtspClauses"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useFciAbortSignatureFlow } from "../../hooks/useFciAbortSignatureFlow"; -import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import customVariables from "../../../../theme/variables"; import QtspClauseListItem from "../../components/QtspClauseListItem"; import { FCI_ROUTES } from "../../navigation/routes"; import { useIODispatch } from "../../../../store/hooks"; import { fciEndRequest, fciStartSigningRequest } from "../../store/actions"; -import { LoadingErrorComponent } from "../../../../components/LoadingErrorComponent"; import { fciPollFilledDocumentErrorSelector, fciPollFilledDocumentReadySelector } from "../../store/reducers/fciPollFilledDocument"; import GenericErrorComponent from "../../components/GenericErrorComponent"; import LinkedText from "../../components/LinkedText"; -import { servicePreferenceSelector } from "../../../../store/reducers/entities/services/servicePreference"; -import { loadServicePreference } from "../../../../store/actions/services/servicePreference"; +import { servicePreferenceSelector } from "../../../services/store/reducers/servicePreference"; +import { loadServicePreference } from "../../../services/store/actions"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { useFciCheckService } from "../../hooks/useFciCheckService"; -import { isServicePreferenceResponseSuccess } from "../../../../types/services/ServicePreferenceResponse"; +import { isServicePreferenceResponseSuccess } from "../../../services/types/ServicePreferenceResponse"; import { fciMetadataServiceIdSelector } from "../../store/reducers/fciMetadata"; -import ScreenContent from "../../../../components/screens/ScreenContent"; import { trackFciUxConversion } from "../../analytics"; +import LoadingComponent from "../../components/LoadingComponent"; import { fciEnvironmentSelector } from "../../store/reducers/fciEnvironment"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; const FciQtspClausesScreen = () => { const dispatch = useIODispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const [clausesChecked, setClausesChecked] = React.useState(0); const servicePreference = useSelector(servicePreferenceSelector); const qtspClausesSelector = useSelector(fciQtspClausesSelector); @@ -76,20 +79,19 @@ const FciQtspClausesScreen = () => { useFciAbortSignatureFlow(); const openUrl = (url: string) => { - navigation.navigate(FCI_ROUTES.DOC_PREVIEW, { - documentUrl: url, - enableAnnotationRendering: true + navigation.navigate(FCI_ROUTES.MAIN, { + screen: FCI_ROUTES.DOC_PREVIEW, + params: { + documentUrl: url + } }); }; - const LoadingComponent = () => ( - - ); + useHeaderSecondLevel({ + title: I18n.t("features.fci.title"), + contextualHelp: emptyContextualHelp, + supportRequest: true + }); if (fciPollFilledDocumentError && !isPollFilledDocumentReady) { return ( @@ -102,7 +104,7 @@ const FciQtspClausesScreen = () => { /> ); } else if (!isPollFilledDocumentReady) { - return ; + return ; } const renderClausesFields = () => ( @@ -116,9 +118,7 @@ const FciQtspClausesScreen = () => { `${index}`} - ItemSeparatorComponent={() => ( - - )} + ItemSeparatorComponent={() => } renderItem={({ item }) => ( { )} ListFooterComponent={ <> - + { ); - const cancelButtonProps = { - block: true, - light: false, - bordered: true, + const cancelButtonProps: ButtonSolidProps = { onPress: showAbort, - title: I18n.t("global.buttons.cancel") + label: I18n.t("global.buttons.cancel"), + accessibilityLabel: I18n.t("global.buttons.cancel") }; - const continueButtonProps = { - block: true, - primary: true, + const continueButtonProps: ButtonSolidProps = { disabled: clausesChecked !== qtspClausesSelector.length, onPress: () => { if (isServiceActive) { @@ -168,33 +164,28 @@ const FciQtspClausesScreen = () => { showCheckService(); } }, - title: I18n.t("global.buttons.continue") + label: I18n.t("global.buttons.continue"), + accessibilityLabel: I18n.t("global.buttons.continue") }; return ( - - - - - {renderClausesFields()} - - + <> + + +

{I18n.t("features.fci.qtspTos.title")}

+ + {I18n.t("features.fci.qtspTos.subTitle")} + {renderClausesFields()} +
-
+ {fciAbortSignature} {fciCheckService} -
+ ); }; export default FciQtspClausesScreen; diff --git a/ts/features/fci/screens/valid/FciSignatureFieldsScreen.tsx b/ts/features/fci/screens/valid/FciSignatureFieldsScreen.tsx index c71ad3c4bce..97b9646c356 100644 --- a/ts/features/fci/screens/valid/FciSignatureFieldsScreen.tsx +++ b/ts/features/fci/screens/valid/FciSignatureFieldsScreen.tsx @@ -1,13 +1,20 @@ import * as React from "react"; -import { View, SafeAreaView, SectionList, Platform } from "react-native"; +import { View, SectionList, ScrollView } from "react-native"; import { useSelector } from "react-redux"; -import { StackActions, useNavigation } from "@react-navigation/native"; +import { Route, StackActions, useRoute } from "@react-navigation/native"; import * as RA from "fp-ts/lib/ReadonlyArray"; import * as O from "fp-ts/lib/Option"; import { constFalse, increment, pipe } from "fp-ts/lib/function"; -import { IconButton, IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; +import { + ButtonSolidProps, + FooterWithButtons, + H2, + H4, + IconButton, + IOColors, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIODispatch } from "../../../../store/hooks"; @@ -16,12 +23,9 @@ import { fciSignatureDetailDocumentsSelector } from "../../store/reducers/fciSignatureRequest"; import { DocumentDetailView } from "../../../../../definitions/fci/DocumentDetailView"; -import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; -import { FciParamsList } from "../../navigation/params"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import SignatureFieldItem from "../../components/SignatureFieldItem"; -import { H3 } from "../../../../components/core/typography/H3"; import { SignatureField } from "../../../../../definitions/fci/SignatureField"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { FCI_ROUTES } from "../../navigation/routes"; import { fciDocumentSignaturesSelector } from "../../store/reducers/fciDocumentSignatures"; import { @@ -40,7 +44,6 @@ import { getSectionListData, orderSignatureFields } from "../../utils/signatureFields"; -import ScreenContent from "../../../../components/screens/ScreenContent"; import { LightModalContext } from "../../../../components/ui/LightModal"; import DocumentWithSignature from "../../components/DocumentWithSignature"; import GenericErrorComponent from "../../components/GenericErrorComponent"; @@ -50,17 +53,19 @@ import { } from "../../analytics"; import { useFciSignatureFieldInfo } from "../../hooks/useFciSignatureFieldInfo"; import { fciEnvironmentSelector } from "../../store/reducers/fciEnvironment"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; export type FciSignatureFieldsScreenNavigationParams = Readonly<{ documentId: DocumentDetailView["id"]; currentDoc: number; }>; -const FciSignatureFieldsScreen = ( - props: IOStackNavigationRouteProps -) => { - const currentDoc = props.route.params.currentDoc; - const docId = props.route.params.documentId; +const FciSignatureFieldsScreen = () => { + const { currentDoc, documentId: docId } = + useRoute< + Route<"FCI_SIGNATURE_FIELDS", FciSignatureFieldsScreenNavigationParams> + >().params; + const documentsSelector = useSelector(fciSignatureDetailDocumentsSelector); const signatureFieldsSelector = useSelector( fciDocumentSignatureFieldsSelector(docId) @@ -70,7 +75,7 @@ const FciSignatureFieldsScreen = ( ); const fciEnvironment = useSelector(fciEnvironmentSelector); const dispatch = useIODispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const [isClausesChecked, setIsClausesChecked] = React.useState(false); const [isError, setIsError] = React.useState(false); const { showModal, hideModal } = React.useContext(LightModalContext); @@ -165,9 +170,9 @@ const FciSignatureFieldsScreen = ( flexDirection: "row" }} > -

+

{clauseLabel} -

+ {/* Show info icon and signature field info only for unfair clauses @@ -190,7 +195,6 @@ const FciSignatureFieldsScreen = ( const renderSignatureFields = () => ( ); - const cancelButtonProps = { - block: true, - light: false, - bordered: true, + const cancelButtonProps: ButtonSolidProps = { onPress: present, - title: I18n.t("global.buttons.cancel") + label: I18n.t("global.buttons.cancel"), + accessibilityLabel: I18n.t("global.buttons.cancel") }; - const continueButtonProps = { - block: true, - primary: true, + const continueButtonProps: ButtonSolidProps = { disabled: !isClausesChecked, onPress: () => { if (currentDoc < documentsSelector.length - 1) { @@ -243,20 +243,21 @@ const FciSignatureFieldsScreen = ( }); } }, - title: + accessibilityLabel: + currentDoc < documentsSelector.length - 1 + ? I18n.t("global.buttons.continue") + : "Firma", + label: currentDoc < documentsSelector.length - 1 ? I18n.t("global.buttons.continue") : "Firma" }; - const customGoBack: React.ReactElement = ( - - ); + useHeaderSecondLevel({ + title: I18n.t("features.fci.title"), + supportRequest: true, + contextualHelp: emptyContextualHelp + }); if (isError) { return ( @@ -270,25 +271,19 @@ const FciSignatureFieldsScreen = ( } return ( - - - - - {renderSignatureFields()} - - - + + +

{I18n.t("features.fci.signatureFields.title")}

+ + {renderSignatureFields()} +
+ {fciAbortSignature} -
+ ); }; export default FciSignatureFieldsScreen; diff --git a/ts/features/fci/screens/valid/FciSignatureRequestsScreen.tsx b/ts/features/fci/screens/valid/FciSignatureRequestsScreen.tsx index 434e231d406..e05434beaf7 100644 --- a/ts/features/fci/screens/valid/FciSignatureRequestsScreen.tsx +++ b/ts/features/fci/screens/valid/FciSignatureRequestsScreen.tsx @@ -1,8 +1,6 @@ import * as React from "react"; -import { SafeAreaView, View, SectionList } from "react-native"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import ScreenContent from "../../../../components/screens/ScreenContent"; +import { SectionList, ScrollView } from "react-native"; +import { H2, IOStyles } from "@pagopa/io-app-design-system"; import SignatureRequestItem from "../../components/SignatureRequestItem"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { fciSignaturesListSelector } from "../../store/reducers/fciSignaturesList"; @@ -24,6 +22,8 @@ import { } from "../../../zendesk/store/actions"; import { ToolEnum } from "../../../../../definitions/content/AssistanceToolConfig"; import { SignatureRequestListView } from "../../../../../definitions/fci/SignatureRequestListView"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; const FciSignatureRequestsScreen = () => { const dispatch = useIODispatch(); @@ -62,6 +62,12 @@ const FciSignatureRequestsScreen = () => { dispatch(fciSignaturesListRequest.request()); }, [dispatch]); + useHeaderSecondLevel({ + title: I18n.t("features.fci.requests.header"), + contextualHelp: emptyContextualHelp, + supportRequest: true + }); + const renderSignatureRequests = () => ( ({ @@ -82,18 +88,10 @@ const FciSignatureRequestsScreen = () => { return ( - - - - - {renderSignatureRequests()} - - - - + +

{I18n.t("features.fci.requests.title")}

+ {renderSignatureRequests()} +
); }; diff --git a/ts/features/fci/screens/valid/FciThankyouScreen.tsx b/ts/features/fci/screens/valid/FciThankyouScreen.tsx index 5773969ffbb..35c6a8f7b2e 100644 --- a/ts/features/fci/screens/valid/FciThankyouScreen.tsx +++ b/ts/features/fci/screens/valid/FciThankyouScreen.tsx @@ -1,23 +1,23 @@ import * as React from "react"; -import { SafeAreaView } from "react-native"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { constNull } from "fp-ts/lib/function"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; +import { + ButtonSolidProps, + FooterWithButtons, + IOStyles, + Pictogram +} from "@pagopa/io-app-design-system"; +import { View } from "react-native"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { LoadingErrorComponent } from "../../../../components/LoadingErrorComponent"; import { fciSignatureSelector } from "../../store/reducers/fciSignature"; import GenericErrorComponent from "../../components/GenericErrorComponent"; -import paymentCompleted from "../../../../../img/pictograms/payment-completed.png"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; import I18n from "../../../../i18n"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; -import { InfoScreenComponent } from "../../../../components/infoScreen/InfoScreenComponent"; -import { renderInfoRasterImage } from "../../../../components/infoScreen/imageRendering"; import { fciEndRequest, fciStartRequest } from "../../store/actions"; import { trackFciUxSuccess } from "../../analytics"; import { TypeEnum as ClauseTypeEnum } from "../../../../../definitions/fci/Clause"; import { fciDocumentSignaturesSelector } from "../../store/reducers/fciDocumentSignatures"; import { getClausesCountByTypes } from "../../utils/signatureFields"; +import LoadingComponent from "../../components/LoadingComponent"; +import { InfoScreenComponent } from "../../components/InfoScreenComponent"; import { fciEnvironmentSelector } from "../../store/reducers/fciEnvironment"; const FciThankyouScreen = () => { @@ -26,13 +26,8 @@ const FciThankyouScreen = () => { const fciEnvironment = useIOSelector(fciEnvironmentSelector); const dispatch = useIODispatch(); - const LoadingComponent = () => ( - + const LoadingView = () => ( + ); const ErrorComponent = () => ( @@ -46,34 +41,33 @@ const FciThankyouScreen = () => { /> ); - const SuccessComponent = () => ( - - + const SuccessComponent = () => { + const continueButtonProps: ButtonSolidProps = { + onPress: () => dispatch(fciEndRequest()), + label: I18n.t("features.fci.thankYouPage.cta"), + accessibilityLabel: I18n.t("features.fci.thankYouPage.cta"), + testID: "FciTypCloseButton" + }; + return ( + } title={I18n.t("features.fci.thankYouPage.title")} body={I18n.t("features.fci.thankYouPage.content")} /> dispatch(fciEndRequest()), - title: I18n.t("features.fci.thankYouPage.cta"), - block: true, - light: false, - bordered: true, - testID: "FciTypSuccessFooterButton" - }} + primary={{ type: "Solid", buttonProps: continueButtonProps }} /> - - - ); + + ); + }; return pot.fold( fciCreateSignatureSelector, - () => , - () => , - () => , + () => , + () => , + () => , _ => , _ => { trackFciUxSuccess( @@ -87,8 +81,8 @@ const FciThankyouScreen = () => { ); return ; }, - () => , - () => , + () => , + () => , _ => ); }; diff --git a/ts/features/fims/navigation/navigator.tsx b/ts/features/fims/navigation/navigator.tsx index dbd46c66945..5ab2b9d246d 100644 --- a/ts/features/fims/navigation/navigator.tsx +++ b/ts/features/fims/navigation/navigator.tsx @@ -2,10 +2,11 @@ import { createStackNavigator } from "@react-navigation/stack"; import * as React from "react"; import { PathConfigMap } from "@react-navigation/native"; import FimsWebviewScreen from "../screens/FimsWebviewScreen"; +import { AppParamsList } from "../../../navigation/params/AppParamsList"; import FIMS_ROUTES from "./routes"; import { FimsParamsList } from "./params"; -export const fimsLinkingOptions: PathConfigMap = { +export const fimsLinkingOptions: PathConfigMap = { [FIMS_ROUTES.MAIN]: { path: "fims", screens: { @@ -19,8 +20,7 @@ const Stack = createStackNavigator(); export const FimsNavigator = () => ( diff --git a/ts/features/idpay/barcode/navigation/navigator.tsx b/ts/features/idpay/barcode/navigation/navigator.tsx index 6ee9ee46417..d25351fcfff 100644 --- a/ts/features/idpay/barcode/navigation/navigator.tsx +++ b/ts/features/idpay/barcode/navigation/navigator.tsx @@ -8,8 +8,7 @@ const Stack = createStackNavigator(); export const IdPayBarcodeNavigator = () => ( { const route = useRoute(); const { initiativeId } = route.params; - const navigation = useNavigation(); + const navigation = useIONavigation(); const barcodePot = useIOSelector(state => idPayBarcodeByInitiativeIdSelector(state)(initiativeId) ); const navigateToInitiativeDetails = () => navigation.navigate(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { - route: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, - routeParams: { initiativeId } + screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, + params: { initiativeId } }); if (pot.isLoading(barcodePot)) { @@ -170,7 +171,7 @@ const SuccessContent = ({ goBack, barcode }: SuccessContentProps) => { const BarcodeExpiredContent = ({ initiativeId }: BarcodeExpiredContentProps) => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const dispatch = useIODispatch(); const { goBack } = navigation; const ctaClickHandler = () => { diff --git a/ts/features/idpay/code/navigation/navigator.tsx b/ts/features/idpay/code/navigation/navigator.tsx index 1f6a688e665..ae97c4f06da 100644 --- a/ts/features/idpay/code/navigation/navigator.tsx +++ b/ts/features/idpay/code/navigation/navigator.tsx @@ -16,8 +16,7 @@ const Stack = createStackNavigator(); export const IdPayCodeNavigator = () => ( = { /** * IDPay initiative onboarding */ diff --git a/ts/features/idpay/configuration/navigation/navigator.tsx b/ts/features/idpay/configuration/navigation/navigator.tsx index 5ddfeed56fd..680e6e5ec9b 100644 --- a/ts/features/idpay/configuration/navigation/navigator.tsx +++ b/ts/features/idpay/configuration/navigation/navigator.tsx @@ -47,8 +47,7 @@ export const IDPayConfigurationNavigator = () => ( { const configurationMachine = useConfigurationMachineService(); const [state, send] = useActor(configurationMachine); + const theme = useIOTheme(); + const isLoading = state.tags.has(LOADING_TAG); const handleContinuePress = () => { @@ -78,12 +79,24 @@ const InitiativeConfigurationIntroScreen = () => { const requiredDataItems: ReadonlyArray = [ { - icon: , + icon: ( + + ), title: I18n.t("idpay.configuration.intro.requiredData.ibanTitle"), subTitle: I18n.t("idpay.configuration.intro.requiredData.ibanSubtitle") }, { - icon: , + icon: ( + + ), title: I18n.t("idpay.configuration.intro.requiredData.instrumentTitle"), subTitle: I18n.t( "idpay.configuration.intro.requiredData.instrumentSubtitle" diff --git a/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts b/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts index 895f259b75a..57d5f37f112 100644 --- a/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts +++ b/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle */ import * as O from "fp-ts/lib/Option"; import * as p from "@pagopa/ts-commons/lib/pot"; +import { IOToast } from "@pagopa/io-app-design-system"; import { createActionsImplementation } from "../actions"; import { ConfigurationMode, Context } from "../context"; import { @@ -12,7 +13,6 @@ import ROUTES from "../../../../../navigation/routes"; import { IDPayDetailsRoutes } from "../../../details/navigation"; import { InitiativeFailureType } from "../failure"; import I18n from "../../../../../i18n"; -import { IOToast } from "../../../../../components/Toast"; import { refreshSessionToken } from "../../../../fastLogin/store/actions/tokenRefreshActions"; jest.mock("../../../../../utils/showToast", () => ({ @@ -45,7 +45,7 @@ const T_FAILURE = InitiativeFailureType.GENERIC; describe("IDPay Configuration machine actions", () => { const actions = createActionsImplementation( - navigation as IOStackNavigationProp, + navigation as IOStackNavigationProp, dispatch ); diff --git a/ts/features/idpay/configuration/xstate/actions.ts b/ts/features/idpay/configuration/xstate/actions.ts index a12d9785196..c13a055d15c 100644 --- a/ts/features/idpay/configuration/xstate/actions.ts +++ b/ts/features/idpay/configuration/xstate/actions.ts @@ -1,6 +1,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { IOToast } from "../../../../components/Toast"; +import { IOToast } from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { AppParamsList, diff --git a/ts/features/idpay/configuration/xstate/provider.tsx b/ts/features/idpay/configuration/xstate/provider.tsx index 840139ee51c..c8bd425a151 100644 --- a/ts/features/idpay/configuration/xstate/provider.tsx +++ b/ts/features/idpay/configuration/xstate/provider.tsx @@ -1,4 +1,3 @@ -import { useNavigation } from "@react-navigation/native"; import { useInterpret } from "@xstate/react"; import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; @@ -16,10 +15,7 @@ import { pagoPaApiUrlPrefixTest } from "../../../../config"; import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; import { @@ -62,7 +58,7 @@ const IDPayConfigurationMachineProvider = (props: Props) => { O.getOrElse(() => PreferredLanguageEnum.it_IT) ); - const navigation = useNavigation>(); + const navigation = useIONavigation(); if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); diff --git a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx index 63a98432e36..19f7549fd7f 100644 --- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx +++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx @@ -24,11 +24,11 @@ import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import ROUTES from "../../../../navigation/routes"; import { format } from "../../../../utils/dates"; import { Table, TableRow } from "../../common/components/Table"; import { formatNumberCurrencyOrDefault } from "../../common/utils/strings"; import { IDPayUnsubscriptionRoutes } from "../../unsubscription/navigation/navigator"; +import { SERVICES_ROUTES } from "../../../services/navigation/routes"; import { InitiativeRulesInfoBox, InitiativeRulesInfoBoxSkeleton @@ -176,8 +176,8 @@ const BeneficiaryDetailsContent = (props: BeneficiaryDetailsProps) => { NonEmptyString.decode(beneficiaryDetails.serviceId), O.fromEither, O.map(serviceId => - navigation.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICE_DETAIL, + navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICE_DETAIL, params: { serviceId } }) ) diff --git a/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx b/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx index de39263b47f..fb4c2142526 100644 --- a/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx +++ b/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx @@ -13,7 +13,7 @@ import { Body } from "../../../../components/core/typography/Body"; import { H4 } from "../../../../components/core/typography/H4"; import { Link } from "../../../../components/core/typography/Link"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import Markdown from "../../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../../i18n"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; @@ -26,7 +26,7 @@ const InitiativeRulesInfoBox = (props: Props) => { const { bottomSheet, present, dismiss } = useIOBottomSheetAutoresizableModal( { - component: {content}, + component: {content}, title: I18n.t("idpay.initiative.beneficiaryDetails.infoModal.title"), footer: ( diff --git a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx index 5e240c73e60..5d3ac8969cd 100644 --- a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx +++ b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx @@ -1,13 +1,14 @@ -import { useNavigation } from "@react-navigation/core"; import React from "react"; import { View } from "react-native"; import { Alert, VSpacer } from "@pagopa/io-app-design-system"; +import { NavigatorScreenParams } from "@react-navigation/native"; import { StatusEnum as InitiativeStatusEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; import { IDPayConfigurationParamsList, IDPayConfigurationRoutes } from "../../configuration/navigation/navigator"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; type StatusWithAlert = Exclude< InitiativeStatusEnum, @@ -22,7 +23,7 @@ type Props = { }; const MissingConfigurationAlert = (props: Props) => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const { status, initiativeId } = props; @@ -46,11 +47,11 @@ const MissingConfigurationAlert = (props: Props) => { const handleNavigation = () => { navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: screen[status], + screen: screen[status] as keyof IDPayConfigurationParamsList, params: { initiativeId } - }); + } as NavigatorScreenParams); }; return ( diff --git a/ts/features/idpay/details/components/TimelineOperationListItem.tsx b/ts/features/idpay/details/components/TimelineOperationListItem.tsx index 4199efb050b..edd27489c2b 100644 --- a/ts/features/idpay/details/components/TimelineOperationListItem.tsx +++ b/ts/features/idpay/details/components/TimelineOperationListItem.tsx @@ -48,7 +48,7 @@ import { import I18n from "../../../../i18n"; import { hoursAndMinutesToAccessibilityReadableFormat } from "../../../../utils/accessibility"; import { localeDateFormat } from "../../../../utils/locale"; -import { getBadgeTextByTransactionStatus } from "../../../walletV3/common/utils"; +import { getBadgeTextByTransactionStatus } from "../../../payments/common/utils"; import { formatAbsNumberAmountOrDefault } from "../../common/utils/strings"; export type TimelineOperationListItemProps = WithTestID< diff --git a/ts/features/idpay/details/screens/IdPayOperationsListScreen.tsx b/ts/features/idpay/details/screens/IdPayOperationsListScreen.tsx index db3e6ae36bd..50708d42147 100644 --- a/ts/features/idpay/details/screens/IdPayOperationsListScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayOperationsListScreen.tsx @@ -2,6 +2,7 @@ import { ContentWrapper, Divider, HSpacer, + IOToast, VSpacer } from "@pagopa/io-app-design-system"; import { useRoute } from "@react-navigation/core"; @@ -12,7 +13,6 @@ import React from "react"; import { ActivityIndicator, FlatList, StyleSheet } from "react-native"; import Placeholder from "rn-placeholder"; import { OperationListDTO } from "../../../../../definitions/idpay/OperationListDTO"; -import { IOToast } from "../../../../components/Toast"; import { Body } from "../../../../components/core/typography/Body"; import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../../../i18n"; @@ -130,9 +130,11 @@ export const IdPayOperationsListScreen = () => { return ( diff --git a/ts/features/idpay/onboarding/components/OnboardingDescriptionMarkdown.tsx b/ts/features/idpay/onboarding/components/OnboardingDescriptionMarkdown.tsx index 70e7cf8d6ed..52aa30f4632 100644 --- a/ts/features/idpay/onboarding/components/OnboardingDescriptionMarkdown.tsx +++ b/ts/features/idpay/onboarding/components/OnboardingDescriptionMarkdown.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { View } from "react-native"; import Placeholder from "rn-placeholder"; import { VSpacer } from "@pagopa/io-app-design-system"; -import Markdown from "../../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; type Props = { description: string; @@ -28,7 +28,9 @@ const OnboardingDescriptionMarkdown = (props: Props) => { return ( {!isLoaded && } - {description} + + {description} + ); } diff --git a/ts/features/idpay/onboarding/navigation/navigator.tsx b/ts/features/idpay/onboarding/navigation/navigator.tsx index 1356290a946..40c72636c7b 100644 --- a/ts/features/idpay/onboarding/navigation/navigator.tsx +++ b/ts/features/idpay/onboarding/navigation/navigator.tsx @@ -43,8 +43,7 @@ export const IDPayOnboardingNavigator = () => ( initialRouteName={ IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS } - headerMode={"none"} - screenOptions={{ gestureEnabled: isGestureEnabled }} + screenOptions={{ gestureEnabled: isGestureEnabled, headerShown: false }} > { const serviceId = useSelector(machine, selectServiceId); const serviceName = pipe( - useIOSelector(state => serviceByIdSelector(state, serviceId as ServiceId)), + useIOSelector(state => + serviceByIdPotSelector(state, serviceId as ServiceId) + ), pot.toOption, O.fold( () => I18n.t("idpay.onboarding.PDNDPrerequisites.fallbackInitiativeName"), @@ -69,14 +71,14 @@ export const PDNDPrerequisitesScreen = () => { "idpay.onboarding.PDNDPrerequisites.prerequisites.info.header" ), component: ( - + {I18n.t( "idpay.onboarding.PDNDPrerequisites.prerequisites.info.body", { provider: authority } )} - + ), footer: ( diff --git a/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts b/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts index 772195a5b91..40698389315 100644 --- a/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts +++ b/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts @@ -54,8 +54,11 @@ const T_INITIATIVE_INFO_DTO: InitiativeDataDTO = { describe("IDPay Onboarding machine actions", () => { const actions = createActionsImplementation( - rootNavigation as IOStackNavigationProp, - onboardingNavigation as IDPayOnboardingStackNavigationProp, + rootNavigation as IOStackNavigationProp, + onboardingNavigation as IDPayOnboardingStackNavigationProp< + IDPayOnboardingParamsList, + keyof IDPayOnboardingParamsList + >, dispatch ); diff --git a/ts/features/idpay/onboarding/xstate/provider.tsx b/ts/features/idpay/onboarding/xstate/provider.tsx index 8eb86422cc9..6f42814cdea 100644 --- a/ts/features/idpay/onboarding/xstate/provider.tsx +++ b/ts/features/idpay/onboarding/xstate/provider.tsx @@ -9,10 +9,7 @@ import { idPayApiUatBaseUrl, idPayTestToken } from "../../../../config"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; import { @@ -54,10 +51,13 @@ const IDPayOnboardingMachineProvider = (props: Props) => { const sessionInfo = useIOSelector(sessionInfoSelector); - const rootNavigation = useNavigation>(); + const rootNavigation = useIONavigation(); const onboardingNavigation = useNavigation< - IDPayOnboardingStackNavigationProp + IDPayOnboardingStackNavigationProp< + IDPayOnboardingParamsList, + keyof IDPayOnboardingParamsList + > >(); if (O.isNone(sessionInfo)) { diff --git a/ts/features/idpay/payment/navigation/navigator.tsx b/ts/features/idpay/payment/navigation/navigator.tsx index d2e1aef52ed..1f78fc47a2c 100644 --- a/ts/features/idpay/payment/navigation/navigator.tsx +++ b/ts/features/idpay/payment/navigation/navigator.tsx @@ -32,8 +32,7 @@ export const IDPayPaymentNavigator = () => ( { return ( - +

{I18n.t("idpay.payment.manualInput.title")}

diff --git a/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx index 8b17a364edb..eaed669f2bf 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx @@ -4,7 +4,7 @@ import { Alert } from "react-native"; import ReactNativeHapticFeedback, { HapticFeedbackTypes } from "react-native-haptic-feedback"; -import { IOToast } from "../../../../components/Toast"; +import { IOToast } from "@pagopa/io-app-design-system"; import { useOpenDeepLink } from "../../../../hooks/useOpenDeepLink"; import I18n from "../../../../i18n"; import { diff --git a/ts/features/idpay/payment/xstate/provider.tsx b/ts/features/idpay/payment/xstate/provider.tsx index 8c05923eda9..0f058dc5767 100644 --- a/ts/features/idpay/payment/xstate/provider.tsx +++ b/ts/features/idpay/payment/xstate/provider.tsx @@ -1,4 +1,3 @@ -import { useNavigation } from "@react-navigation/native"; import { useInterpret } from "@xstate/react"; import * as O from "fp-ts/lib/Option"; import React from "react"; @@ -9,10 +8,7 @@ import { idPayTestToken } from "../../../../config"; import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; @@ -38,7 +34,7 @@ const IDPayPaymentMachineProvider = (props: Props) => { const sessionInfo = useIOSelector(sessionInfoSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - const navigation = useNavigation>(); + const navigation = useIONavigation(); if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); diff --git a/ts/features/idpay/unsubscription/navigation/navigator.tsx b/ts/features/idpay/unsubscription/navigation/navigator.tsx index 7460468f989..bc7ea864e67 100644 --- a/ts/features/idpay/unsubscription/navigation/navigator.tsx +++ b/ts/features/idpay/unsubscription/navigation/navigator.tsx @@ -46,7 +46,7 @@ export const IDPayUnsubscriptionNavigator = () => { initialRouteName={ IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION } - headerMode={"none"} + screenOptions={{ headerShown: false }} > { O.getOrElse(() => PreferredLanguageEnum.it_IT) ); - const navigation = useNavigation>(); + const navigation = useIONavigation(); if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); diff --git a/ts/features/idpay/wallet/components/IdPayCard.tsx b/ts/features/idpay/wallet/components/IdPayCard.tsx new file mode 100644 index 00000000000..1489d87709a --- /dev/null +++ b/ts/features/idpay/wallet/components/IdPayCard.tsx @@ -0,0 +1,84 @@ +import { + Avatar, + H3, + H6, + LabelSmallAlt, + VSpacer +} from "@pagopa/io-app-design-system"; +import { format } from "date-fns"; +import React from "react"; +import { ImageURISource, StyleSheet, View } from "react-native"; +import WalletCardShape from "../../../../../img/features/idpay/wallet_card.svg"; +import I18n from "../../../../i18n"; +import { formatNumberAmount } from "../../../../utils/stringBuilder"; + +export type IdPayCardProps = { + name: string; + avatarSource: ImageURISource; + amount: number; + expireDate: Date; +}; + +/** + * Component that renders the ID PAy card in the wallet + */ +export const IdPayCard = (props: IdPayCardProps) => ( + + + + + + + +
+ {props.name} +
+ +
+ + Disponibile +

+ {formatNumberAmount(props.amount, true, "right")} +

+
+ + {I18n.t("bonusCard.validUntil", { + endDate: format(props.expireDate, "MM/YY") + })} + +
+
+); + +const styles = StyleSheet.create({ + container: { + aspectRatio: 16 / 10 + }, + card: { + position: "absolute", + left: 0, + right: 0, + top: 0, + bottom: 0 + }, + content: { + flex: 1, + paddingTop: 12, + paddingRight: 12, + paddingBottom: 16, + paddingLeft: 16, + justifyContent: "space-between" + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center" + } +}); diff --git a/ts/features/idpay/wallet/components/IdPayWalletCard.tsx b/ts/features/idpay/wallet/components/IdPayWalletCard.tsx new file mode 100644 index 00000000000..1c6770b2f2a --- /dev/null +++ b/ts/features/idpay/wallet/components/IdPayWalletCard.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Pressable } from "react-native"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { withWalletCardBaseComponent } from "../../../newWallet/components/WalletCardBaseComponent"; +import { IDPayDetailsRoutes } from "../../details/navigation"; +import { IdPayCard, IdPayCardProps } from "./IdPayCard"; + +export type IdPayWalletCardProps = IdPayCardProps & { + initiativeId: string; +}; + +const WrappedIdPayCard = (props: IdPayWalletCardProps) => { + const navigation = useIONavigation(); + + const { initiativeId, ...cardProps } = props; + + const handleOnPress = () => { + navigation.navigate(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { + screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, + params: { + initiativeId + } + }); + }; + + return ( + + + + ); +}; + +/** + * Wrapper component which adds wallet capabilites to the IdPayCard component + */ +export const IdPayWalletCard = withWalletCardBaseComponent(WrappedIdPayCard); diff --git a/ts/features/idpay/wallet/components/__tests__/IdPayCard.test.tsx b/ts/features/idpay/wallet/components/__tests__/IdPayCard.test.tsx new file mode 100644 index 00000000000..9d511d920eb --- /dev/null +++ b/ts/features/idpay/wallet/components/__tests__/IdPayCard.test.tsx @@ -0,0 +1,19 @@ +import { render } from "@testing-library/react-native"; +import * as React from "react"; +import { IdPayCard } from "../IdPayCard"; + +describe("IdPayCard", () => { + it("should match the snapshot", () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/ts/features/idpay/wallet/components/__tests__/__snapshots__/IdPayCard.test.tsx.snap b/ts/features/idpay/wallet/components/__tests__/__snapshots__/IdPayCard.test.tsx.snap new file mode 100644 index 00000000000..9ff519e0e87 --- /dev/null +++ b/ts/features/idpay/wallet/components/__tests__/__snapshots__/IdPayCard.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IdPayCard should match the snapshot 1`] = ` + + + + + + + + + 18 app + + + + + + + + + + Disponibile + + + 9,999.00 € + + + + Valida fino al 12/23 + + + +`; diff --git a/ts/features/idpay/wallet/saga/__test__/handleGetWallet.test.ts b/ts/features/idpay/wallet/saga/__test__/handleGetWallet.test.ts index 8098858ebb6..1be5a0479d5 100644 --- a/ts/features/idpay/wallet/saga/__test__/handleGetWallet.test.ts +++ b/ts/features/idpay/wallet/saga/__test__/handleGetWallet.test.ts @@ -6,6 +6,7 @@ import { WalletDTO } from "../../../../../../definitions/idpay/WalletDTO"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; import { idPayWalletGet } from "../../store/actions"; import { handleGetIDPayWallet } from "../handleGetWallet"; +import { walletAddCards } from "../../../../newWallet/store/actions/cards"; const mockedWallet: WalletDTO = { initiativeList: [] }; @@ -25,6 +26,8 @@ describe("handleGetIDPayWallet", () => { .next() .call(withRefreshApiCall, getWallet(), idPayWalletGet.request()) .next(E.right({ status: 200, value: mockedWallet })) + .put(walletAddCards([])) + .next() .put(idPayWalletGet.success(mockedWallet)) .next() .isDone(); diff --git a/ts/features/idpay/wallet/saga/handleGetWallet.ts b/ts/features/idpay/wallet/saga/handleGetWallet.ts index 5eca064526f..593dfc0d062 100644 --- a/ts/features/idpay/wallet/saga/handleGetWallet.ts +++ b/ts/features/idpay/wallet/saga/handleGetWallet.ts @@ -8,6 +8,7 @@ import { readablePrivacyReport } from "../../../../utils/reporters"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; import { IDPayClient } from "../../common/api/client"; import { idPayWalletGet } from "../store/actions"; +import { walletAddCards } from "../../../newWallet/store/actions/cards"; /** * Handle the remote call to retrieve the IDPay wallet @@ -35,7 +36,24 @@ export function* handleGetIDPayWallet( if (E.isRight(getWalletResult)) { if (getWalletResult.right.status === 200) { // handled success - yield* put(idPayWalletGet.success(getWalletResult.right.value)); + const initiatives = getWalletResult.right.value; + yield* put( + walletAddCards( + initiatives.initiativeList.map(initiative => ({ + type: "idPay", + category: "bonus", + key: `idpay_${initiative.initiativeId}`, + initiativeId: initiative.initiativeId, + name: initiative.initiativeName || "", + amount: initiative.amount || 0, + avatarSource: { + uri: initiative.logoURL + }, + expireDate: initiative.endDate + })) + ) + ); + yield* put(idPayWalletGet.success(initiatives)); return; } // not handled error codes diff --git a/ts/features/idpay/wallet/screens/IdPayInstrumentInitiativesScreen.tsx b/ts/features/idpay/wallet/screens/IdPayInstrumentInitiativesScreen.tsx index f65801060b5..2c67989077e 100644 --- a/ts/features/idpay/wallet/screens/IdPayInstrumentInitiativesScreen.tsx +++ b/ts/features/idpay/wallet/screens/IdPayInstrumentInitiativesScreen.tsx @@ -5,14 +5,13 @@ import { pipe } from "fp-ts/lib/function"; import React from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import { HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { H1 } from "../../../../components/core/typography/H1"; import { H4 } from "../../../../components/core/typography/H4"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; import TypedI18n from "../../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../../navigation/params/WalletParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import customVariables from "../../../../theme/variables"; import { IdPayInstrumentInitiativesList } from "../components/IdPayInstrumentInitiativesList"; @@ -26,13 +25,14 @@ export type IdPayInstrumentInitiativesScreenRouteParams = { idWallet: string; }; -type Props = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_IDPAY_INITIATIVE_LIST" ->; - -export const IdPayInstrumentInitiativesScreen = (props: Props) => { - const { idWallet } = props.route.params; +export const IdPayInstrumentInitiativesScreen = () => { + const { idWallet } = + useRoute< + Route< + "WALLET_IDPAY_INITIATIVE_LIST", + IdPayInstrumentInitiativesScreenRouteParams + > + >().params; const dispatch = useIODispatch(); const initiatives = useIOSelector(idPayInitiativesFromInstrumentSelector); diff --git a/ts/features/lollipop/playgrounds/LollipopPlayground.tsx b/ts/features/lollipop/playgrounds/LollipopPlayground.tsx index f3a43ca6fc1..0d0e0fb16ba 100644 --- a/ts/features/lollipop/playgrounds/LollipopPlayground.tsx +++ b/ts/features/lollipop/playgrounds/LollipopPlayground.tsx @@ -1,23 +1,23 @@ -import * as O from "fp-ts/lib/Option"; +import { ContentWrapper } from "@pagopa/io-app-design-system"; import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; import React, { useCallback } from "react"; import { SafeAreaView, ScrollView } from "react-native"; -import { ContentWrapper } from "@pagopa/io-app-design-system"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; +import { ProblemJson } from "../../../../definitions/lollipop/ProblemJson"; +import { SignMessageResponse } from "../../../../definitions/lollipop/SignMessageResponse"; import { IOStyles } from "../../../components/core/variables/IOStyles"; +import { apiUrlPrefix } from "../../../config"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { useIOSelector } from "../../../store/hooks"; import { sessionTokenSelector } from "../../../store/reducers/authentication"; import { createLollipopClient, signMessage } from "../api/backend"; -import { useIOSelector } from "../../../store/hooks"; import { lollipopKeyTagSelector, lollipopPublicKeySelector } from "../store/reducers/lollipop"; import { toThumbprint } from "../utils/crypto"; -import { apiUrlPrefix } from "../../../config"; -import { SignMessageResponse } from "../../../../definitions/lollipop/SignMessageResponse"; -import { ProblemJson } from "../../../../definitions/lollipop/ProblemJson"; import LollipopPlaygroundContent from "./LollipopPlaygroundContent"; export type LollipopPlaygroundState = { @@ -39,6 +39,10 @@ const LollipopPlayground = () => { const maybePublicKey = useIOSelector(lollipopPublicKeySelector); const maybeSessionToken = O.fromNullable(useIOSelector(sessionTokenSelector)); + useHeaderSecondLevel({ + title: "Lollipop Playground" + }); + const lollipopClient = useCallback( (signBody: boolean) => pipe( @@ -125,29 +129,27 @@ const LollipopPlayground = () => { ); return ( - - - - - - onSignButtonPress(body, state.doSignBody) - } - onCheckBoxPress={v => { - setState({ - ...state, - doSignBody: v - }); - }} - onClearButtonPress={() => { - setState(INITIAL_STATE); - }} - playgroundState={state} - /> - - - - + + + + + onSignButtonPress(body, state.doSignBody) + } + onCheckBoxPress={v => { + setState({ + ...state, + doSignBody: v + }); + }} + onClearButtonPress={() => { + setState(INITIAL_STATE); + }} + playgroundState={state} + /> + + + ); }; diff --git a/ts/features/lollipop/playgrounds/LollipopPlaygroundContent.tsx b/ts/features/lollipop/playgrounds/LollipopPlaygroundContent.tsx index cb820043047..c87c1853102 100644 --- a/ts/features/lollipop/playgrounds/LollipopPlaygroundContent.tsx +++ b/ts/features/lollipop/playgrounds/LollipopPlaygroundContent.tsx @@ -1,26 +1,27 @@ -import * as O from "fp-ts/lib/Option"; -import React from "react"; -import { View, StyleSheet, TextInput } from "react-native"; import { Alert, ButtonOutline, ButtonSolid, + CheckboxLabel, HSpacer, + IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import { CheckBox } from "../../../components/core/selection/checkbox/CheckBox"; -import { Label } from "../../../components/core/typography/Label"; -import { maybeNotNullyString } from "../../../utils/strings"; +import * as O from "fp-ts/lib/Option"; +import React from "react"; +import { StyleSheet, TextInput, View } from "react-native"; import { WithTestID } from "../../../types/WithTestID"; +import { maybeNotNullyString } from "../../../utils/strings"; import { LollipopPlaygroundState } from "./LollipopPlayground"; const styles = StyleSheet.create({ textInput: { textAlignVertical: "top", // Prop supported on Android only - padding: 10, + padding: 12, borderWidth: 1, height: 120, - borderRadius: 4 + borderRadius: 8, + borderColor: IOColors["grey-450"] }, column: { flexDirection: "column", @@ -64,12 +65,11 @@ const LollipopPlaygroundContent = (props: Props) => { /> - - - diff --git a/ts/features/lollipop/utils/crypto.ts b/ts/features/lollipop/utils/crypto.ts index 469fbb2eb99..135f16d2842 100644 --- a/ts/features/lollipop/utils/crypto.ts +++ b/ts/features/lollipop/utils/crypto.ts @@ -1,8 +1,17 @@ -import { PublicKey, CryptoError } from "@pagopa/io-react-native-crypto"; +import { + PublicKey, + CryptoError, + isKeyStrongboxBacked +} from "@pagopa/io-react-native-crypto"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { jwkThumbprintByEncoding } from "jwk-thumbprint"; +import { isAndroid } from "../../../utils/platform"; +import { + trackLollipopIsKeyStrongboxBackedFailure, + trackLollipopIsKeyStrongboxBackedSuccess +} from "../../../utils/analytics"; import { DEFAULT_LOLLIPOP_HASH_ALGORITHM_CLIENT } from "./login"; export type KeyInfo = { @@ -29,3 +38,18 @@ export const toThumbprint = (key: O.Option) => thumbprint => thumbprint ) ); + +/** + * Check if the key is backed by Strongbox and track the result only on Android. + * @param keyTag - the keyTag of the key to check. + */ +export const handleIsKeyStrongboxBacked = async (keyTag?: string) => { + if (keyTag && isAndroid) { + try { + const isStrongBoxBacked = await isKeyStrongboxBacked(keyTag); + trackLollipopIsKeyStrongboxBackedSuccess(isStrongBoxBacked); + } catch (e) { + trackLollipopIsKeyStrongboxBackedFailure(toCryptoError(e).message); + } + } +}; diff --git a/ts/features/messages/__e2e__/messages.e2e.ts b/ts/features/messages/__e2e__/messages.e2e.ts index a9d2037e1b3..7b9ed5604b7 100644 --- a/ts/features/messages/__e2e__/messages.e2e.ts +++ b/ts/features/messages/__e2e__/messages.e2e.ts @@ -8,31 +8,25 @@ describe("Messages Screen", () => { await ensureLoggedIn(); }); - describe("when the user is already logged in", () => { - it("should load the user's messages", async () => { - await waitFor(element(by.id("MessageList_inbox"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + it("when the user is already logged in, it should load the user's messages", async () => { + await waitFor(element(by.id("MessageList_inbox"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // The webserver is sending us exactly 20 messages with consecutive IDs - // in reverse order. - // We test that the first one is visible on the UI and that the last one - // exists (but is not visible) + // The webserver is sending us exactly 20 messages with consecutive IDs + // in reverse order. + // We test that the first one is visible on the UI and that the last one + // exists (but is not visible) - await waitFor( - element(by.id(`MessageListItem_00000000000000000000000020`)) - ) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + await waitFor(element(by.id(`MessageListItem_00000000000000000000000020`))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // check for infinite scrolling - await element(by.id(`MessageList_inbox`)).scrollTo("bottom"); + // check for infinite scrolling + await element(by.id(`MessageList_inbox`)).scrollTo("bottom"); - await waitFor( - element(by.id(`MessageListItem_00000000000000000000000008`)) - ) - .toExist() - .withTimeout(e2eWaitRenderTimeout); - }); + await waitFor(element(by.id(`MessageListItem_00000000000000000000000008`))) + .toExist() + .withTimeout(e2eWaitRenderTimeout); }); }); diff --git a/ts/features/messages/__mocks__/attachment.ts b/ts/features/messages/__mocks__/attachment.ts index f4523587e7d..d9f86c81fcf 100644 --- a/ts/features/messages/__mocks__/attachment.ts +++ b/ts/features/messages/__mocks__/attachment.ts @@ -1,24 +1,19 @@ -import { Byte } from "../types/digitalInformationUnit"; -import { UIMessageId, UIAttachment, UIAttachmentId } from "../types"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; import { ATTACHMENT_CATEGORY } from "../types/attachmentCategory"; -import { message_1 } from "./message"; -export const mockPdfAttachment: UIAttachment = { - messageId: message_1.id as UIMessageId, - id: "1" as UIAttachmentId, - displayName: "invoice.pdf", - contentType: "application/pdf", - size: 1959520 as Byte, - resourceUrl: { href: "https://www.invoicepdf.com/invoice.pdf" }, +export const mockPdfAttachment: ThirdPartyAttachment = { + id: "1" as NonEmptyString, + name: "invoice.pdf" as NonEmptyString, + content_type: "application/pdf" as NonEmptyString, + url: "https://www.invoicepdf.com/invoice.pdf" as NonEmptyString, category: ATTACHMENT_CATEGORY.DOCUMENT }; -export const mockOtherAttachment: UIAttachment = { - messageId: message_1.id as UIMessageId, - id: "2" as UIAttachmentId, - displayName: "image.png", - contentType: "other", - size: 125952 as Byte, - resourceUrl: { href: "htts://www.randomImage.com/image.png" }, +export const mockOtherAttachment: ThirdPartyAttachment = { + id: "2" as NonEmptyString, + name: "image.png" as NonEmptyString, + content_type: "other" as NonEmptyString, + url: "htts://www.randomImage.com/image.png" as NonEmptyString, category: ATTACHMENT_CATEGORY.F24 }; diff --git a/ts/features/messages/__mocks__/message.ts b/ts/features/messages/__mocks__/message.ts index 5a77b7449b1..08bae54cd57 100644 --- a/ts/features/messages/__mocks__/message.ts +++ b/ts/features/messages/__mocks__/message.ts @@ -1,5 +1,6 @@ import { CreatedMessageWithContentAndAttachments } from "../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; import { FiscalCode } from "../../../../definitions/backend/FiscalCode"; +import { PaymentDataWithRequiredPayee } from "../../../../definitions/backend/PaymentDataWithRequiredPayee"; import { UIMessageDetails } from "../types"; import { toUIMessageDetails } from "../store/reducers/transformers"; @@ -22,6 +23,37 @@ export const message_1: CreatedMessageWithContentAndAttachments = { sender_service_id: service_1.service_id }; +export const messageWithValidPayment: CreatedMessageWithContentAndAttachments = + { + ...message_1, + created_at: new Date("2024-01-01T14:16:41Z"), + content: { + ...message_1.content, + payment_data: { + notice_number: "075970423479738892", + amount: 698, + invalid_after_due_date: true, + payee: { fiscal_code: "00000000003" } + } as PaymentDataWithRequiredPayee + } + }; + +export const messageWithExpairedPayment: CreatedMessageWithContentAndAttachments = + { + ...message_1, + created_at: new Date("2024-01-01T14:16:41Z"), + content: { + ...message_1.content, + due_date: new Date("2024-02-03T14:16:41Z"), + payment_data: { + notice_number: "075970423479738892", + amount: 698, + invalid_after_due_date: true, + payee: { fiscal_code: "00000000003" } + } as PaymentDataWithRequiredPayee + } + }; + export const paymentValidInvalidAfterDueDate: CreatedMessageWithContentAndAttachments = { created_at: "2021-11-23T13:29:54.771Z", diff --git a/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx b/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx index c20551a8d11..d87c67299bc 100644 --- a/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx +++ b/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx @@ -16,7 +16,7 @@ import { } from "../../store/actions"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { downloadPotForMessageAttachmentSelector } from "../../store/reducers/downloads"; -import { UIAttachment, UIMessageId } from "../../types"; +import { UIMessageId } from "../../types"; import variables from "../../../../theme/variables"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { isIos } from "../../../../utils/platform"; @@ -24,11 +24,16 @@ import { isStrictNone } from "../../../../utils/pot"; import { share } from "../../../../utils/share"; import { showToast } from "../../../../utils/showToast"; import { confirmButtonProps } from "../../../../components/buttons/ButtonConfigurations"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { + attachmentContentType, + attachmentDisplayName +} from "../../store/reducers/transformers"; import LegacyPdfViewer from "./LegacyPdfViewer"; type Props = { messageId: UIMessageId; - attachment: UIAttachment; + attachment: ThirdPartyAttachment; enableDownloadAttachment?: boolean; onLoadComplete?: () => void; onPDFError?: () => void; @@ -74,9 +79,14 @@ const renderError = (title: string, body: string) => ( ); const renderPDF = ( + name: string, + mimeType: string, downloadPath: string, isPDFError: boolean, - props: Omit, + props: Omit< + Props, + "enableDownloadAttachment" | "attachment" | "messageId" | "onPDFError" + >, onPDFLoadingError: () => void ) => ( <> @@ -93,7 +103,8 @@ const renderPDF = ( /> )} {renderFooter( - props.attachment, + name, + mimeType, downloadPath, props.onShare, props.onOpen, @@ -103,7 +114,8 @@ const renderPDF = ( ); const renderFooter = ( - attachment: UIAttachment, + name: string, + mimeType: string, downloadPath: string, onShare?: () => void, onOpen?: () => void, @@ -140,9 +152,9 @@ const renderFooter = ( onDownload?.(); ReactNativeBlobUtil.MediaCollection.copyToMediaStore( { - name: attachment.displayName, + name, parentFolder: "", - mimeType: attachment.contentType + mimeType }, "Download", downloadPath @@ -150,7 +162,7 @@ const renderFooter = ( .then(_ => { showToast( I18n.t("messagePDFPreview.savedAtLocation", { - name: attachment.displayName + name }), "success" ); @@ -165,7 +177,7 @@ const renderFooter = ( () => { onOpen?.(); ReactNativeBlobUtil.android - .actionViewIntent(downloadPath, attachment.contentType) + .actionViewIntent(downloadPath, mimeType) .catch(_ => { showToast(I18n.t("messagePDFPreview.errors.opening")); }); @@ -194,10 +206,7 @@ export const LegacyMessageAttachmentPreview = ({ const attachment = props.attachment; const attachmentId = attachment.id; const downloadPot = useIOSelector(state => - downloadPotForMessageAttachmentSelector(state, { - messageId, - id: attachmentId - }) + downloadPotForMessageAttachmentSelector(state, messageId, attachmentId) ); // This component handles the attachment blob download only if // it is a generic attachment (not a PN one, since that flow @@ -239,8 +248,9 @@ export const LegacyMessageAttachmentPreview = ({ if (shouldDownloadAttachment) { dispatch( downloadAttachment.request({ - ...attachment, - skipMixpanelTrackingOnFailure: true + attachment, + messageId, + skipMixpanelTrackingOnFailure: false }) ); } else if ( @@ -259,6 +269,7 @@ export const LegacyMessageAttachmentPreview = ({ downloadPot, dispatch, enableDownloadAttachment, + messageId, navigation, shouldDownloadAttachment ]); @@ -267,6 +278,9 @@ export const LegacyMessageAttachmentPreview = ({ const shouldDisplayPDFPreview = pot.isSome(downloadPot) && !pot.isError(downloadPot); + const name = attachmentDisplayName(attachment); + const mimeType = attachmentContentType(attachment); + return ( ; + mimeType: string; + name: string; +}; -const renderFooter = ( - attachment: UIAttachment, - downloadPath: string, - isPN: boolean, - attachmentCategory?: string -) => +const MessageAttachmentFooter = ({ + attachmentCategory, + downloadPath, + isPN, + mimeType, + name +}: MessageAttachmentFooterProps) => isIos ? ( { onShare(isPN, attachmentCategory); ReactNativeBlobUtil.ios.presentOptionsMenu(downloadPath); @@ -59,31 +62,31 @@ const renderFooter = ( /> ) : ( { onShare(isPN, attachmentCategory); share(`file://${downloadPath}`, undefined, false)().catch(_ => { IOToast.show(I18n.t("messagePDFPreview.errors.sharing")); }); }, - label: I18n.t("global.buttons.share") + label: I18n.t("messagePDFPreview.share") } }} - third={{ - type: "Outline", + primary={{ + type: "Solid", buttonProps: { - accessibilityLabel: I18n.t("messagePDFPreview.save"), + accessibilityLabel: I18n.t("messagePDFPreview.saveAccessibility"), onPress: () => { onDownload(isPN, attachmentCategory); ReactNativeBlobUtil.MediaCollection.copyToMediaStore( { - name: attachment.displayName, + name, parentFolder: "", - mimeType: attachment.contentType + mimeType }, "Download", downloadPath @@ -91,7 +94,7 @@ const renderFooter = ( .then(_ => { IOToast.show( I18n.t("messagePDFPreview.savedAtLocation", { - name: attachment.displayName + name }) ); }) @@ -102,21 +105,6 @@ const renderFooter = ( label: I18n.t("messagePDFPreview.save") } }} - secondary={{ - type: "Solid", - buttonProps: { - accessibilityLabel: I18n.t("messagePDFPreview.open"), - onPress: () => { - onOpen(isPN, attachmentCategory); - ReactNativeBlobUtil.android - .actionViewIntent(downloadPath, attachment.contentType) - .catch(_ => { - IOToast.error(I18n.t("messagePDFPreview.errors.opening")); - }); - }, - label: I18n.t("messagePDFPreview.open") - } - }} /> ); @@ -162,15 +150,6 @@ const onShare = (isPN: boolean, attachmentCategory?: string) => ) ); -const onOpen = (isPN: boolean, attachmentCategory?: string) => - pipe( - isPN, - B.fold( - () => trackThirdPartyMessageAttachmentUserAction("open"), - () => trackPNAttachmentOpen(attachmentCategory) - ) - ); - const onDownload = (isPN: boolean, attachmentCategory?: string) => pipe( isPN, @@ -180,13 +159,19 @@ const onDownload = (isPN: boolean, attachmentCategory?: string) => ) ); -export const MessageAttachment = ( - props: IOStackNavigationRouteProps< - MessagesParamsList, - "MESSAGE_DETAIL_ATTACHMENT" - > -): React.ReactElement => { - const { messageId, attachmentId, isPN, serviceId } = props.route.params; +export type MessageAttachmentProps = { + messageId: UIMessageId; + attachmentId: string; + isPN: boolean; + serviceId?: ServiceId; +}; + +export const MessageAttachment = ({ + attachmentId, + isPN, + messageId, + serviceId +}: MessageAttachmentProps) => { const [isPDFRenderingError, setIsPDFRenderingError] = useState(false); const downloadedAttachment = useIOSelector(state => @@ -201,11 +186,6 @@ export const MessageAttachment = ( onPDFError(messageId, isPN, serviceId, attachmentCategory); }, [attachmentCategory, messageId, isPN, serviceId]); - useHeaderSecondLevel({ - title: I18n.t("messagePDFPreview.title"), - supportRequest: true - }); - if (!attachmentOpt || !downloadPathOpt) { return ( ); } + + const name = attachmentDisplayName(attachmentOpt); + const mimeType = attachmentContentType(attachmentOpt); + return ( <> {isPDFRenderingError ? ( @@ -230,7 +214,13 @@ export const MessageAttachment = ( onLoadComplete={() => onLoadComplete(isPN, attachmentCategory)} /> )} - {renderFooter(attachmentOpt, downloadPathOpt, isPN, attachmentCategory)} + ); }; diff --git a/ts/features/messages/components/MessageAttachment/PdfViewer.tsx b/ts/features/messages/components/MessageAttachment/PdfViewer.tsx index b26f69052c2..04ec664299f 100644 --- a/ts/features/messages/components/MessageAttachment/PdfViewer.tsx +++ b/ts/features/messages/components/MessageAttachment/PdfViewer.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import Pdf from "react-native-pdf"; import { IOColors } from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; @@ -31,19 +31,25 @@ export const PdfViewer = ({ isLoading={isLoading} loadingCaption={I18n.t("messageDetails.attachments.loading")} > - { - setIsLoading(false); - onLoadComplete?.(...args); - }} - onError={(...args) => { - setIsLoading(false); - onError?.(...args); - }} - /> + + { + setIsLoading(false); + onLoadComplete?.(...args); + }} + onError={(...args) => { + setIsLoading(false); + onError?.(...args); + }} + /> + ); }; diff --git a/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx b/ts/features/messages/components/MessageAttachment/__test__/LegacyMessageAttachmentPreview.test.tsx similarity index 73% rename from ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx rename to ts/features/messages/components/MessageAttachment/__test__/LegacyMessageAttachmentPreview.test.tsx index d91181746df..7bde6c1ecac 100644 --- a/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx +++ b/ts/features/messages/components/MessageAttachment/__test__/LegacyMessageAttachmentPreview.test.tsx @@ -2,19 +2,20 @@ import React from "react"; import { View } from "react-native"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { createStore } from "redux"; -import { applicationChangeState } from "../../../../store/actions/application"; -import { appReducer } from "../../../../store/reducers"; -import { GlobalState } from "../../../../store/reducers/types"; -import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; -import { Downloads } from "../../store/reducers/downloads"; -import { mockPdfAttachment } from "../../__mocks__/attachment"; -import I18n from "../../../../i18n"; -import { LegacyMessageAttachmentPreview } from "../MessageAttachment/LegacyMessageAttachmentPreview"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { Downloads } from "../../../store/reducers/downloads"; +import { mockPdfAttachment } from "../../../__mocks__/attachment"; +import I18n from "../../../../../i18n"; +import { LegacyMessageAttachmentPreview } from "../LegacyMessageAttachmentPreview"; +import { messageId_1 } from "../../../__mocks__/messages"; const mockOpen = jest.fn(); const mockPdfViewer = ; -jest.mock("../MessageAttachment/LegacyPdfViewer", () => () => mockPdfViewer); +jest.mock("../LegacyPdfViewer", () => () => mockPdfViewer); describe("MessageAttachmentPreview", () => { describe("when enableDownloadAttachment is false", () => { @@ -22,12 +23,12 @@ describe("MessageAttachmentPreview", () => { const { component } = renderComponent( { enableDownloadAttachment: false, - messageId: mockPdfAttachment.messageId, + messageId: messageId_1, attachment: mockPdfAttachment, onOpen: mockOpen }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.some({ path: "path", attachment: mockPdfAttachment @@ -43,12 +44,12 @@ describe("MessageAttachmentPreview", () => { const { component } = renderComponent( { enableDownloadAttachment: false, - messageId: mockPdfAttachment.messageId, + messageId: messageId_1, attachment: mockPdfAttachment, onOpen: mockOpen }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.some({ path: "path", attachment: mockPdfAttachment diff --git a/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap b/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap index 07e6e5f8ead..514e3cb14bd 100644 --- a/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap +++ b/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap @@ -140,6 +140,12 @@ exports[`PdfViewer should match the snapshot 1`] = ` /> @@ -245,6 +274,16 @@ exports[`PdfViewer should match the snapshot 1`] = ` }, ] } - /> + > + + `; diff --git a/ts/features/messages/components/MessageAttachments.tsx b/ts/features/messages/components/MessageAttachments.tsx deleted file mode 100644 index 981f42200d5..00000000000 --- a/ts/features/messages/components/MessageAttachments.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { View } from "react-native"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { VSpacer, WithTestID } from "@pagopa/io-app-design-system"; -import { UIAttachment } from "../types"; -import { ContentTypeValues } from "../types/contentType"; -import { useAttachmentDownload } from "../hooks/useAttachmentDownload"; -import { ModuleAttachment, ModuleAttachmentProps } from "./ModuleAttachment"; - -type PartialProps = { - downloadAttachmentBeforePreview?: boolean; - openPreview: (attachment: UIAttachment) => void; -}; - -type MessageAttachmentProps = { - attachment: UIAttachment; -} & PartialProps; - -type MessageAttachmentsProps = WithTestID< - { - attachments: ReadonlyArray; - } & PartialProps ->; - -const getFormatByContentType = ( - contentType: UIAttachment["contentType"] -): ModuleAttachmentProps["format"] => { - switch (contentType) { - case ContentTypeValues.applicationPdf: - return "pdf"; - default: - return "doc"; - } -}; - -const AttachmentItem = ({ - attachment, - openPreview, - downloadAttachmentBeforePreview -}: MessageAttachmentProps) => { - const { downloadPot, onAttachmentSelect } = useAttachmentDownload( - attachment, - downloadAttachmentBeforePreview, - openPreview - ); - - return ( - - ); -}; - -export const MessageAttachments = ({ - attachments = [], - testID, - ...rest -}: MessageAttachmentsProps) => ( - - {attachments.map((attachment, index) => ( - - - {index < attachments.length - 1 && } - - ))} - -); diff --git a/ts/features/messages/components/MessageDetail/CalendarEventButton.tsx b/ts/features/messages/components/MessageDetail/CalendarEventButton.tsx index a3688616341..016e3058c68 100644 --- a/ts/features/messages/components/MessageDetail/CalendarEventButton.tsx +++ b/ts/features/messages/components/MessageDetail/CalendarEventButton.tsx @@ -26,7 +26,7 @@ import { GlobalState } from "../../../../store/reducers/types"; import { openAppSettings } from "../../../../utils/appSettings"; import { checkAndRequestPermission, - isEventInCalendar, + legacyIsEventInCalendar, removeCalendarEventFromDeviceCalendar, saveCalendarEvent, searchEventInCalendar @@ -90,7 +90,9 @@ class CalendarEventButton extends React.PureComponent { }); return; } - const mayBeInCalendar = await isEventInCalendar(calendarEvent.eventId)(); + const mayBeInCalendar = await legacyIsEventInCalendar( + calendarEvent.eventId + )(); this.setState({ isEventInDeviceCalendar: pipe( mayBeInCalendar, @@ -356,7 +358,7 @@ class CalendarEventButton extends React.PureComponent { const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => ({ preferredCalendar: preferredCalendarSelector(state), - calendarEvent: calendarEventByMessageIdSelector(ownProps.message.id)(state) + calendarEvent: calendarEventByMessageIdSelector(state, ownProps.message.id) }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/ts/features/messages/components/MessageDetail/ContactsListItem.tsx b/ts/features/messages/components/MessageDetail/ContactsListItem.tsx new file mode 100644 index 00000000000..ce2f084d1dd --- /dev/null +++ b/ts/features/messages/components/MessageDetail/ContactsListItem.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Linking } from "react-native"; +import { Body, ListItemAction, VSpacer } from "@pagopa/io-app-design-system"; +import I18n from "../../../../i18n"; +import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; + +type ContactsListItemProps = { + email?: string; + phone?: string; +}; + +export const ContactsListItem = ({ email, phone }: ContactsListItemProps) => { + const { present, bottomSheet } = useIOBottomSheetAutoresizableModal( + { + component: ( + <> + + {I18n.t("messageDetails.contactsBottomSheet.body")} + + + {email && ( + Linking.openURL(`mailto:${email}`)} + variant="primary" + /> + )} + {phone && ( + Linking.openURL(`tel:${phone}`)} + variant="primary" + /> + )} + + ), + title: I18n.t("messageDetails.contactsBottomSheet.title") + }, + 100 + ); + + return ( + <> + + {bottomSheet} + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/CtaBar.tsx b/ts/features/messages/components/MessageDetail/CtaBar.tsx index d581c8ce534..d7677cdd2fa 100644 --- a/ts/features/messages/components/MessageDetail/CtaBar.tsx +++ b/ts/features/messages/components/MessageDetail/CtaBar.tsx @@ -1,11 +1,9 @@ import * as O from "fp-ts/lib/Option"; import React, { useEffect, useRef } from "react"; import { View, StyleSheet } from "react-native"; -import { useStore } from "react-redux"; import { HSpacer } from "@pagopa/io-app-design-system"; import { CommonServiceMetadata } from "../../../../../definitions/backend/CommonServiceMetadata"; -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { useIODispatch } from "../../../../store/hooks"; +import { useIODispatch, useIOStore } from "../../../../store/hooks"; import { PaymentData, UIMessageDetails, UIMessageId } from "../../types"; import { UIService } from "../../../../store/reducers/entities/services/types"; import variables from "../../../../theme/variables"; @@ -89,16 +87,14 @@ const CtaBar = ({ }: Props): React.ReactElement | null => { const dispatch = useIODispatch(); const shoulCheckForPNOptInMessage = useRef(true); - const store = useStore(); + const store = useIOStore(); const { dueDate, markdown, paymentData, raw: legacyMessage } = messageDetails; - const maybeCtas = getMessageCTA( - markdown, - serviceMetadata, - service?.id as ServiceId + const ctas = O.toUndefined( + getMessageCTA(markdown, serviceMetadata, service?.id) ); const state = store.getState(); - const isPNOptInMessageInfo = isPNOptInMessage(maybeCtas, service, state); + const isPNOptInMessageInfo = isPNOptInMessage(ctas, service?.id, state); const isPNOptIn = isPNOptInMessageInfo.isPNOptInMessage; useEffect(() => { @@ -131,12 +127,12 @@ const CtaBar = ({ ); - const footer2 = O.isSome(maybeCtas) && ( + const footer2 = ctas && ( // Added a wrapper to enable the usage of the component outside the Container of Native Base void; + openPreview: (attachment: ThirdPartyAttachment) => void; }; -type MessageAttachmentProps = { - attachment: UIAttachment; +type LegacyMessageAttachmentProps = { + attachment: ThirdPartyAttachment; + messageId: UIMessageId; } & PartialProps; type LegacyMessageAttachmentsProps = { - attachments: ReadonlyArray; + attachments: ReadonlyArray; + messageId: UIMessageId; } & PartialProps; const styles = StyleSheet.create({ @@ -60,9 +66,7 @@ const styles = StyleSheet.create({ * @param props * @constructor */ -const AttachmentIcon = (props: { - contentType: UIAttachment["contentType"]; -}) => { +const LegacyAttachmentIcon = (props: { contentType: string }) => { switch (props.contentType) { case ContentTypeValues.applicationPdf: return ; @@ -77,12 +81,15 @@ const AttachmentIcon = (props: { * @param props * @constructor */ -const AttachmentItem = (props: MessageAttachmentProps) => { - const { downloadPot, onAttachmentSelect } = useAttachmentDownload( +const LegacyAttachmentItem = (props: LegacyMessageAttachmentProps) => { + const { downloadPot, onAttachmentSelect } = useLegacyAttachmentDownload( props.attachment, + props.messageId, props.downloadAttachmentBeforePreview, props.openPreview ); + const name = attachmentDisplayName(props.attachment); + const mimeType = attachmentContentType(props.attachment); return ( { onPress={onAttachmentSelect} disabled={!!props.disabled || pot.isLoading(downloadPot)} accessible={true} - accessibilityLabel={props.attachment.displayName} + accessibilityLabel={name} accessibilityRole="button" testID="MessageAttachmentTouchable" > - +
{ ellipsizeMode={"middle"} numberOfLines={2} > - {props.attachment.displayName} + {name}
- {typeof props.attachment.size !== "undefined" && ( -
- {formatByte(props.attachment.size)} -
- )}
{pot.isLoading(downloadPot) ? ( {attachments.map((attachment, index) => ( - + {index < attachments.length - 1 && } ))} diff --git a/ts/features/messages/components/ModuleAttachment.tsx b/ts/features/messages/components/MessageDetail/LegacyModuleAttachment.tsx similarity index 92% rename from ts/features/messages/components/ModuleAttachment.tsx rename to ts/features/messages/components/MessageDetail/LegacyModuleAttachment.tsx index 121a64990e2..9e56b8a7569 100644 --- a/ts/features/messages/components/ModuleAttachment.tsx +++ b/ts/features/messages/components/MessageDetail/LegacyModuleAttachment.tsx @@ -30,7 +30,7 @@ import Animated, { withSpring } from "react-native-reanimated"; import Placeholder from "rn-placeholder"; -import I18n from "../../../i18n"; +import I18n from "../../../../i18n"; type PartialProps = WithTestID<{ title: string; @@ -41,11 +41,11 @@ type PartialProps = WithTestID<{ onPress: (event: GestureResponderEvent) => void; }>; -export type ModuleAttachmentProps = PartialProps & +export type LegacyModuleAttachmentProps = PartialProps & Pick; const formatMap: Record< - NonNullable, + NonNullable, IOIcons > = { doc: "docAttach", @@ -78,13 +78,13 @@ const DISABLED_OPACITY = 0.5; const ICON_SIZE: IOIconSizeScale = 32; const MARGIN_SIZE: IOSpacingScale = 16; -const ModuleAttachmentContent = ({ +const LegacyModuleAttachmentContent = ({ isFetching, format, title, subtitle }: Pick< - ModuleAttachmentProps, + LegacyModuleAttachmentProps, "isFetching" | "format" | "title" | "subtitle" >) => { const IconOrActivityIndicatorComponent = () => { @@ -137,7 +137,7 @@ const ModuleAttachmentContent = ({ }; /** - * The `ModuleAttachment` component is a custom button component with an extended outline style. + * The `LegacyModuleAttachment` component is a custom button component with an extended outline style. * It provides an animated scaling effect when pressed. * * @param {boolean} isLoading - If true, displays a skeleton loading component. @@ -149,7 +149,7 @@ const ModuleAttachmentContent = ({ * @param {string} iconName - The icon name to display. * @param {function} onPress - The function to be executed when the item is pressed. */ -export const ModuleAttachment = ({ +export const LegacyModuleAttachment = ({ isLoading = false, isFetching = false, disabled = false, @@ -157,7 +157,7 @@ export const ModuleAttachment = ({ accessibilityLabel, onPress, ...rest -}: ModuleAttachmentProps) => { +}: LegacyModuleAttachmentProps) => { const isPressed: Animated.SharedValue = useSharedValue(0); // Scaling transformation applied when the button is pressed @@ -223,7 +223,7 @@ export const ModuleAttachment = ({ { opacity: disabled ? DISABLED_OPACITY : 1 } ]} > - + ); diff --git a/ts/features/messages/components/MessageDetail/MessageDetailHeader.tsx b/ts/features/messages/components/MessageDetail/MessageDetailHeader.tsx deleted file mode 100644 index ae3acb5fb8a..00000000000 --- a/ts/features/messages/components/MessageDetail/MessageDetailHeader.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { PropsWithChildren } from "react"; -import { - ContentWrapper, - Divider, - H3, - LabelSmall, - VSpacer -} from "@pagopa/io-app-design-system"; -import { localeDateFormat } from "../../../../utils/locale"; -import I18n from "../../../../i18n"; -import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; -import { logosForService } from "../../../../utils/services"; -import { OrganizationHeader } from "./OrganizationHeader"; - -export type MessageDetailHeaderProps = PropsWithChildren<{ - createdAt: Date; - subject: string; - sender?: string; - service?: ServicePublic; -}>; - -const MessageHeaderContent = ({ - subject, - createdAt -}: MessageDetailHeaderProps) => ( - <> -

{subject}

- - - {localeDateFormat( - createdAt, - I18n.t("global.dateFormats.fullFormatShortMonthLiteralWithTime") - )} - - -); - -export const MessageDetailHeader = ({ - children, - service, - ...rest -}: MessageDetailHeaderProps) => ( - - {children} - - - - {service && ( - <> - {/* TODO: update logoUri when MultiImage component will be available in DS */} - - - - )} - -); diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsAttachmentItem.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsAttachmentItem.tsx new file mode 100644 index 00000000000..9f6e8663863 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsAttachmentItem.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { ModuleAttachment, VSpacer } from "@pagopa/io-app-design-system"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { UIMessageId } from "../../types"; +import { useAttachmentDownload } from "../../hooks/useAttachmentDownload"; +import I18n from "../../../../i18n"; + +type MessageDetailsAttachmentItemProps = { + attachment: ThirdPartyAttachment; + bottomSpacer?: boolean; + disabled?: boolean; + isPN?: boolean; + messageId: UIMessageId; + onPreNavigate?: () => void; + serviceId?: ServiceId; +}; + +export const MessageDetailsAttachmentItem = ({ + attachment, + bottomSpacer, + disabled = false, + isPN = false, + messageId, + onPreNavigate = undefined, + serviceId +}: MessageDetailsAttachmentItemProps) => { + const { displayName, isFetching, onModuleAttachmentPress } = + useAttachmentDownload( + messageId, + attachment, + isPN, + serviceId, + onPreNavigate + ); + return ( + <> + void onModuleAttachmentPress()} + title={displayName} + /> + {bottomSpacer && } + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsAttachments.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsAttachments.tsx new file mode 100644 index 00000000000..ab5557ff710 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsAttachments.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { pipe } from "fp-ts/lib/function"; +import * as B from "fp-ts/lib/boolean"; +import { ListItemHeader } from "@pagopa/io-app-design-system"; +import { UIMessageId } from "../../types"; +import { useIOSelector } from "../../../../store/hooks"; +import { thirdPartyMessageAttachments } from "../../store/reducers/thirdPartyById"; +import I18n from "../../../../i18n"; +import { ATTACHMENT_CATEGORY } from "../../types/attachmentCategory"; +import { MessageDetailsAttachmentItem } from "./MessageDetailsAttachmentItem"; + +export type MessageDetailsAttachmentsProps = { + disabled?: boolean; + isPN?: boolean; + messageId: UIMessageId; +}; + +export const MessageDetailsAttachments = ({ + disabled = false, + isPN = false, + messageId +}: MessageDetailsAttachmentsProps) => { + const originalAttachments = useIOSelector(state => + thirdPartyMessageAttachments(state, messageId) + ); + const attachments = pipe( + isPN, + B.fold( + () => originalAttachments, + () => + originalAttachments.filter( + attachment => attachment.category !== ATTACHMENT_CATEGORY.F24 + ) + ) + ); + + const attachmentCount = attachments.length; + if (attachmentCount === 0) { + return null; + } + + return ( + <> + + {attachments.map((attachment, index) => ( + + ))} + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx new file mode 100644 index 00000000000..6f8e4840ed8 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { IOColors, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import { useIOSelector } from "../../../../store/hooks"; +import { serviceMetadataByIdSelector } from "../../../services/store/reducers/servicesById"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { UIMessageId } from "../../types"; +import { ContactsListItem } from "./ContactsListItem"; +import { ShowMoreListItem } from "./ShowMoreListItem"; + +const styles = StyleSheet.create({ + container: { + backgroundColor: IOColors["grey-50"], + paddingBottom: "75%", + marginBottom: "-75%" + } +}); + +export type MessageDetailsFooterProps = { + messageId: UIMessageId; + serviceId: ServiceId; +}; + +export const MessageDetailsFooter = ({ + messageId, + serviceId +}: MessageDetailsFooterProps) => { + const serviceMetadata = useIOSelector(state => + serviceMetadataByIdSelector(state, serviceId) + ); + + return ( + + + {(serviceMetadata?.email || serviceMetadata?.phone) && ( + + )} + + + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx new file mode 100644 index 00000000000..4d13d10d661 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx @@ -0,0 +1,78 @@ +import React, { PropsWithChildren } from "react"; +import { StyleSheet, View } from "react-native"; +import { Divider, H3, LabelSmall, VSpacer } from "@pagopa/io-app-design-system"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { localeDateFormat } from "../../../../utils/locale"; +import I18n from "../../../../i18n"; +import { logosForService } from "../../../../utils/services"; +import { useIOSelector } from "../../../../store/hooks"; +import { serviceByIdPotSelector } from "../../../services/store/reducers/servicesById"; +import { gapBetweenItemsInAGrid } from "../../utils"; +import { OrganizationHeader } from "./OrganizationHeader"; + +const styles = StyleSheet.create({ + tagsWrapper: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "flex-start", + marginHorizontal: -(gapBetweenItemsInAGrid / 2), + marginVertical: -(gapBetweenItemsInAGrid / 2) + } +}); + +export type MessageDetailsHeaderProps = PropsWithChildren<{ + createdAt: Date; + subject: string; + serviceId: ServiceId; +}>; + +const MessageDetailsHeaderContent = ({ + subject, + createdAt +}: Pick) => ( + <> +

{subject}

+ + + {localeDateFormat( + createdAt, + I18n.t("global.dateFormats.fullFormatShortMonthLiteralWithTime") + )} + + +); + +export const MessageDetailsHeader = ({ + children, + serviceId, + ...rest +}: MessageDetailsHeaderProps) => { + const service = pipe( + useIOSelector(state => serviceByIdPotSelector(state, serviceId)), + pot.toOption, + O.toUndefined + ); + + return ( + <> + {children} + + + + + {service && ( + <> + + + + )} + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsPayment.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsPayment.tsx new file mode 100644 index 00000000000..1a0ebef3d90 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsPayment.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { ListItemHeader, VSpacer } from "@pagopa/io-app-design-system"; +import { UIMessageId } from "../../types"; +import { useIOSelector } from "../../../../store/hooks"; +import { messagePaymentDataSelector } from "../../store/reducers/detailsById"; +import I18n from "../../../../i18n"; +import { getRptIdStringFromPaymentData } from "../../utils"; +import { MessagePaymentItem } from "./MessagePaymentItem"; + +type MessageDetailsPaymentProps = { + messageId: UIMessageId; +}; + +export const MessageDetailsPayment = ({ + messageId +}: MessageDetailsPaymentProps) => { + const paymentData = useIOSelector(state => + messagePaymentDataSelector(state, messageId) + ); + + if (!paymentData) { + return null; + } + + const rptId = getRptIdStringFromPaymentData(paymentData); + + return ( + <> + + + + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx new file mode 100644 index 00000000000..09098d972ce --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { ButtonSolid, useIOToast } from "@pagopa/io-app-design-system"; +import { PaymentData, UIMessageId } from "../../types"; +import { useIODispatch } from "../../../../store/hooks"; +import I18n from "../../../../i18n"; +import { + getRptIdStringFromPaymentData, + initializeAndNavigateToWalletForPayment +} from "../../utils"; + +type MessageDetailsPaymentButtonProps = { + messageId: UIMessageId; + paymentData: PaymentData; + canNavigateToPayment: boolean; + isLoading: boolean; +}; + +export const MessageDetailsPaymentButton = ({ + messageId, + paymentData, + canNavigateToPayment, + isLoading +}: MessageDetailsPaymentButtonProps) => { + const dispatch = useIODispatch(); + const toast = useIOToast(); + return ( + + initializeAndNavigateToWalletForPayment( + messageId, + getRptIdStringFromPaymentData(paymentData), + false, + paymentData.amount, + canNavigateToPayment, + dispatch, + false, + () => toast.error(I18n.t("genericError")) + ) + } + fullWidth + loading={isLoading} + /> + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsReminder.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsReminder.tsx new file mode 100644 index 00000000000..1a6f4c8932a --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsReminder.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { VSpacer } from "@pagopa/io-app-design-system"; +import { UIMessageId } from "../../types"; +import { useIOSelector } from "../../../../store/hooks"; +import { paymentExpirationBannerStateSelector } from "../../store/reducers/payments"; +import { MessageDetailsReminderExpiring } from "./MessageDetailsReminderExpiring"; +import { MessageDetailsReminderExpired } from "./MessageDetailsReminderExpired"; + +export type MessageDetailsReminderProps = { + dueDate?: Date; + messageId: UIMessageId; + title: string; +}; + +export const MessageDetailsReminder = ({ + dueDate, + messageId, + title +}: MessageDetailsReminderProps) => { + const reminderVisibility = useIOSelector(state => + paymentExpirationBannerStateSelector(state, messageId) + ); + if (reminderVisibility === "hidden" || !dueDate) { + return null; + } + + const isExpiring = reminderVisibility === "visibleExpiring"; + return ( + <> + {isExpiring ? ( + + ) : ( + + )} + + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpired.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpired.tsx new file mode 100644 index 00000000000..d68caa605c4 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpired.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Alert, IOAlertRadius } from "@pagopa/io-app-design-system"; +import Placeholder from "rn-placeholder"; +import { localeDateFormat } from "../../../../utils/locale"; +import I18n from "../../../../i18n"; + +type MessageDetailsReminderExpiredProps = { + dueDate: Date; + isLoading: boolean; +}; + +export const MessageDetailsReminderExpired = ({ + dueDate, + isLoading +}: MessageDetailsReminderExpiredProps) => + isLoading ? ( + + ) : ( + + ); diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpiring.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpiring.tsx new file mode 100644 index 00000000000..fbe70a9d270 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsReminderExpiring.tsx @@ -0,0 +1,57 @@ +import React, { useCallback } from "react"; +import { Alert } from "@pagopa/io-app-design-system"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIOSelector } from "../../../../store/hooks"; +import { preferredCalendarSelector } from "../../../../store/reducers/persistedPreferences"; +import { UIMessageId } from "../../types"; +import { useMessageReminder } from "../../hooks/useMessageReminder"; +import { localeDateFormat } from "../../../../utils/locale"; +import I18n from "../../../../i18n"; + +type MessageDetailsReminderExpiringProps = { + dueDate: Date; + messageId: UIMessageId; + title: string; +}; + +export const MessageDetailsReminderExpiring = ({ + dueDate, + messageId, + title +}: MessageDetailsReminderExpiringProps) => { + const navigation = useIONavigation(); + const preferredCalendar = useIOSelector(preferredCalendarSelector); + + const navigate = useCallback(() => { + navigation.navigate("MESSAGES_NAVIGATOR", { + screen: "MESSAGE_DETAIL_CALENDAR", + params: { + messageId + } + }); + }, [messageId, navigation]); + + const { isEventInDeviceCalendar, upsertReminder } = useMessageReminder( + messageId, + navigate + ); + return ( + upsertReminder(dueDate, title, preferredCalendar)} + content={I18n.t("features.messages.alert.content", { + date: localeDateFormat( + dueDate, + I18n.t("global.dateFormats.shortFormat") + ), + time: localeDateFormat(dueDate, I18n.t("global.dateFormats.timeFormat")) + })} + /> + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx new file mode 100644 index 00000000000..dba6e46b300 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { IOStyles, buttonSolidHeight } from "@pagopa/io-app-design-system"; +import { View } from "react-native"; +import { UIMessageId } from "../../types"; +import { useIOSelector } from "../../../../store/hooks"; +import { isPaymentsButtonVisibleSelector } from "../../store/reducers/payments"; +import { gapBetweenItemsInAGrid } from "../../utils"; + +type ScrollViewAdditionalSpaceProps = { + messageId: UIMessageId; + hasCTA1: boolean; + hasCTA2: boolean; +}; + +export const MessageDetailsScrollViewAdditionalSpace = ({ + messageId, + hasCTA1, + hasCTA2 +}: ScrollViewAdditionalSpaceProps) => { + const safeAreaInsets = useSafeAreaInsets(); + const isShowingPaymentButton = useIOSelector(state => + isPaymentsButtonVisibleSelector(state, messageId) + ); + const hasAtLeastAButton = isShowingPaymentButton || hasCTA1 || hasCTA2; + + const height = + (hasAtLeastAButton ? IOStyles.footer.paddingBottom : 0) + + (isShowingPaymentButton ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + (hasCTA1 ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + (hasCTA2 ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + gapBetweenItemsInAGrid + + IOStyles.footer.paddingBottom + + safeAreaInsets.bottom; + return ( + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx new file mode 100644 index 00000000000..a7999ec8f0b --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx @@ -0,0 +1,354 @@ +import * as React from "react"; +import { StyleSheet, View } from "react-native"; +import { useLinkTo } from "@react-navigation/native"; +import { + ButtonLink, + ButtonOutline, + ButtonSolid, + IOStyles, + VSpacer, + buttonSolidHeight +} from "@pagopa/io-app-design-system"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { PaymentData, UIMessageId } from "../../types"; +import { messagePaymentDataSelector } from "../../store/reducers/detailsById"; +import { useIOSelector } from "../../../../store/hooks"; +import { + canNavigateToPaymentFromMessageSelector, + paymentsButtonStateSelector +} from "../../store/reducers/payments"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { trackPNOptInMessageAccepted } from "../../../pn/analytics"; +import { handleCtaAction } from "../../utils/messages"; +import { CTA, CTAS } from "../../types/MessageCTA"; +import { MessageDetailsPaymentButton } from "./MessageDetailsPaymentButton"; + +const styles = StyleSheet.create({ + container: { + position: "absolute", + overflow: "hidden", + bottom: 0, + width: "100%" + }, + buttonLinkInFooter: { + height: buttonSolidHeight, + justifyContent: "center", + alignSelf: "center" + } +}); + +type MessageDetailsPaymentButtonProps = { + ctas?: CTAS; + firstCTAIsPNOptInMessage: boolean; + messageId: UIMessageId; + secondCTAIsPNOptInMessage: boolean; + serviceId: ServiceId; +}; + +type FooterPaymentWithDoubleCTA = { + tag: "PaymentWithDoubleCTA"; + cta1: CTA; + cta2: CTA; + paymentData: PaymentData; +}; + +type FooterPaymentWithCTA = { + tag: "PaymentWithCTA"; + cta1: CTA; + paymentData: PaymentData; +}; +type FooterDoubleCTA = { + tag: "DoubleCTA"; + cta1: CTA; + cta2: CTA; +}; +type FooterPayment = { + tag: "Payment"; + paymentData: PaymentData; +}; +type FooterCTA = { + tag: "CTA"; + cta1: CTA; +}; +type FooterNone = { + tag: "None"; +}; +type FooterData = + | FooterPaymentWithDoubleCTA + | FooterPaymentWithCTA + | FooterDoubleCTA + | FooterPayment + | FooterCTA + | FooterNone; + +const isNone = (footerData: FooterData): footerData is FooterNone => + footerData.tag === "None"; + +const foldFooterData = ( + footerData: FooterData, + onPaymentWithDoubleCTA: ( + paymentWithDoubleCTA: FooterPaymentWithDoubleCTA + ) => JSX.Element, + onPaymentWithCTA: (paymentWithCTA: FooterPaymentWithCTA) => JSX.Element, + onDoubleCTA: (doubleCTA: FooterDoubleCTA) => JSX.Element, + onPayment: (paymentCTA: FooterPayment) => JSX.Element, + onCTA: (cta: FooterCTA) => JSX.Element, + onNone: () => JSX.Element | null +) => { + switch (footerData.tag) { + case "PaymentWithDoubleCTA": + return onPaymentWithDoubleCTA(footerData); + case "PaymentWithCTA": + return onPaymentWithCTA(footerData); + case "DoubleCTA": + return onDoubleCTA(footerData); + case "Payment": + return onPayment(footerData); + case "CTA": + return onCTA(footerData); + } + return onNone(); +}; + +const computeFooterData = ( + paymentData: PaymentData | undefined, + paymentButtonStatus: "hidden" | "loading" | "enabled", + ctas: CTAS | undefined +): FooterData => { + const isPaymentButtonVisible = + paymentData && paymentButtonStatus !== "hidden"; + const isCTA1Visible = !!ctas?.cta_1; + const cta2 = ctas?.cta_2; + const isCTA2Visible = !!cta2; + if (isPaymentButtonVisible && isCTA1Visible && isCTA2Visible) { + return { + tag: "PaymentWithDoubleCTA", + cta1: ctas.cta_1, + cta2, + paymentData + }; + } else if (isPaymentButtonVisible && isCTA1Visible) { + return { + tag: "PaymentWithCTA", + cta1: ctas.cta_1, + paymentData + }; + } else if (isCTA1Visible && isCTA2Visible) { + return { + tag: "DoubleCTA", + cta1: ctas.cta_1, + cta2 + }; + } else if (isPaymentButtonVisible) { + return { + tag: "Payment", + paymentData + }; + } else if (isCTA1Visible) { + return { + tag: "CTA", + cta1: ctas.cta_1 + }; + } + return { tag: "None" }; +}; + +const renderPaymentWithDoubleCTA = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean, + cta1: CTA, + cta1IsPNOptInMessage: boolean, + cta2: CTA, + cta2IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + + + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + + onCTAPress(cta2, cta2IsPNOptInMessage)} + /> + + +); +const renderPaymentWithCTA = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean, + cta1: CTA, + cta1IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + + + + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + +); +const renderDoubleCTA = ( + cta1: CTA, + cta1IsPNOptInMessage: boolean, + cta2: CTA, + cta2IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + + onCTAPress(cta2, cta2IsPNOptInMessage)} + /> + + +); +const renderPayment = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean +) => ( + +); +const renderCTA = ( + cta: CTA, + isPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + onCTAPress(cta, isPNOptInMessage)} + /> +); + +export const MessageDetailsStickyFooter = ({ + ctas, + firstCTAIsPNOptInMessage, + messageId, + secondCTAIsPNOptInMessage, + serviceId +}: MessageDetailsPaymentButtonProps) => { + const safeAreaInsets = useSafeAreaInsets(); + const paymentData = useIOSelector(state => + messagePaymentDataSelector(state, messageId) + ); + const paymentButtonStatus = useIOSelector(state => + paymentsButtonStateSelector(state, messageId) + ); + const canNavigateToPayment = useIOSelector(state => + canNavigateToPaymentFromMessageSelector(state) + ); + + const linkTo = useLinkTo(); + const handleOnPress = React.useCallback( + (cta: CTA, isPNOptInMessage: boolean) => { + if (isPNOptInMessage) { + trackPNOptInMessageAccepted(); + } + handleCtaAction(cta, linkTo, serviceId); + }, + [linkTo, serviceId] + ); + + const footerData = computeFooterData(paymentData, paymentButtonStatus, ctas); + if (isNone(footerData)) { + return null; + } + + const isPaymentLoading = paymentButtonStatus === "loading"; + return ( + + {foldFooterData( + footerData, + paymentWithDoubleCTA => + renderPaymentWithDoubleCTA( + messageId, + paymentWithDoubleCTA.paymentData, + canNavigateToPayment, + isPaymentLoading, + paymentWithDoubleCTA.cta1, + firstCTAIsPNOptInMessage, + paymentWithDoubleCTA.cta2, + secondCTAIsPNOptInMessage, + handleOnPress + ), + paymentWithCTA => + renderPaymentWithCTA( + messageId, + paymentWithCTA.paymentData, + canNavigateToPayment, + isPaymentLoading, + paymentWithCTA.cta1, + firstCTAIsPNOptInMessage, + handleOnPress + ), + doubleCTA => + renderDoubleCTA( + doubleCTA.cta1, + firstCTAIsPNOptInMessage, + doubleCTA.cta2, + secondCTAIsPNOptInMessage, + handleOnPress + ), + payment => + renderPayment( + messageId, + payment.paymentData, + canNavigateToPayment, + isPaymentLoading + ), + cta => renderCTA(cta.cta1, firstCTAIsPNOptInMessage, handleOnPress), + () => null + )} + + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsTagBox.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsTagBox.tsx new file mode 100644 index 00000000000..ef1906e3992 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsTagBox.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { gapBetweenItemsInAGrid } from "../../utils"; + +const styles = StyleSheet.create({ + tagWrapper: { + marginHorizontal: gapBetweenItemsInAGrid / 2, + marginVertical: gapBetweenItemsInAGrid / 2 + } +}); + +export type MessageDetailsTagBoxProps = { + children: React.ReactNode; +}; + +export const MessageDetailsTagBox = ({ + children +}: MessageDetailsTagBoxProps) => ( + {children} +); diff --git a/ts/features/messages/components/MessageDetail/MessageMarkdown.tsx b/ts/features/messages/components/MessageDetail/MessageMarkdown.tsx index 6dd81298d80..f0a3985b03c 100644 --- a/ts/features/messages/components/MessageDetail/MessageMarkdown.tsx +++ b/ts/features/messages/components/MessageDetail/MessageMarkdown.tsx @@ -4,9 +4,14 @@ import { Omit } from "@pagopa/ts-commons/lib/types"; import React from "react"; - import customVariables from "../../../../theme/variables"; -import Markdown, { MarkdownProps } from "../../../../components/ui/Markdown"; +import { + Markdown, + MarkdownProps +} from "../../../../components/ui/Markdown/Markdown"; +import { isDesignSystemEnabledSelector } from "../../../../store/reducers/persistedPreferences"; +import { useIOSelector } from "../../../../store/hooks"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; type Props = Omit; @@ -55,8 +60,11 @@ img { } `; -const MessageMarkdown: React.SFC = props => ( - -); - -export default MessageMarkdown; +export const MessageMarkdown = (props: Props) => { + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + return isDesignSystemEnabled ? ( + + ) : ( + + ); +}; diff --git a/ts/features/pn/components/MessagePaymentItem.tsx b/ts/features/messages/components/MessageDetail/MessagePaymentItem.tsx similarity index 67% rename from ts/features/pn/components/MessagePaymentItem.tsx rename to ts/features/messages/components/MessageDetail/MessagePaymentItem.tsx index 92684d5c64d..060b0f36027 100644 --- a/ts/features/pn/components/MessagePaymentItem.tsx +++ b/ts/features/messages/components/MessageDetail/MessagePaymentItem.tsx @@ -1,45 +1,53 @@ -import { pipe } from "fp-ts/lib/function"; +import { + ModulePaymentNotice, + PaymentNoticeStatus, + VSpacer, + useIOToast +} from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import React, { useCallback, useEffect } from "react"; import { View } from "react-native"; import { useDispatch, useStore } from "react-redux"; +import { PaymentAmount } from "../../../../../definitions/backend/PaymentAmount"; +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; import { - ModulePaymentNotice, - PaymentNoticeStatus, - VSpacer -} from "@pagopa/io-app-design-system"; -import I18n from "i18n-js"; -import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; -import { UIMessageId } from "../../messages/types"; -import { getRptIdStringFromPayment } from "../utils/rptId"; -import { GlobalState } from "../../../store/reducers/types"; -import { - paymentStatusForUISelector, - shouldUpdatePaymentSelector -} from "../store/reducers/payments"; -import { useIOSelector } from "../../../store/hooks"; -import { updatePaymentForMessage } from "../store/actions"; -import { RemoteValue, fold } from "../../../common/model/RemoteValue"; -import { PaymentRequestsGetResponse } from "../../../../definitions/backend/PaymentRequestsGetResponse"; -import { Detail_v2Enum } from "../../../../definitions/backend/PaymentProblemJson"; + RemoteValue, + fold, + isError +} from "../../../../common/model/RemoteValue"; +import I18n from "../../../../i18n"; +import { useIOSelector } from "../../../../store/hooks"; +import { GlobalState } from "../../../../store/reducers/types"; +import { format } from "../../../../utils/dates"; import { cleanTransactionDescription, getV2ErrorMainType -} from "../../../utils/payment"; -import { getBadgeTextByPaymentNoticeStatus } from "../../messages/utils/strings"; -import { format } from "../../../utils/dates"; +} from "../../../../utils/payment"; import { centsToAmount, formatNumberAmount -} from "../../../utils/stringBuilder"; -import { useIOToast } from "../../../components/Toast"; -import { initializeAndNavigateToWalletForPayment } from "../utils"; +} from "../../../../utils/stringBuilder"; +import { updatePaymentForMessage } from "../../store/actions"; +import { + canNavigateToPaymentFromMessageSelector, + paymentStatusForUISelector, + shouldUpdatePaymentSelector +} from "../../store/reducers/payments"; +import { UIMessageId } from "../../types"; +import { initializeAndNavigateToWalletForPayment } from "../../utils"; +import { getBadgeTextByPaymentNoticeStatus } from "../../utils/strings"; type MessagePaymentItemProps = { - index: number; + hideExpirationDate?: boolean; + index?: number; + isPNPayment?: boolean; messageId: UIMessageId; - payment: NotificationPaymentInfo; noSpaceOnTop?: boolean; + noticeNumber: string; + paymentAmount?: PaymentAmount; + rptId: string; willNavigateToPayment?: () => void; }; @@ -87,7 +95,8 @@ const modulePaymentNoticeForUndefinedOrLoadingPayment = () => ( ); const modulePaymentNoticeFromPaymentStatus = ( - noticeCode: string, + hideExpirationDate: boolean, + noticeNumber: string, paymentStatus: RemoteValue, paymentCallback: () => void ) => @@ -99,6 +108,7 @@ const modulePaymentNoticeFromPaymentStatus = ( const dueDateOrUndefined = pipe( payablePayment.dueDate, O.fromNullable, + O.filter(_ => !hideExpirationDate), O.map( dueDate => `${I18n.t("wallet.firstTransactionSummary.dueDate")} ${format( @@ -128,15 +138,15 @@ const modulePaymentNoticeFromPaymentStatus = ( ); }, processedPaymentDetails => { - const formattedPaymentNoticeCode = noticeCode + const formattedPaymentNoticeNumber = noticeNumber .replace(/(\d{4})/g, "$1 ") .trim(); const { paymentNoticeStatus, badgeText } = processedUIPaymentFromDetailV2Enum(processedPaymentDetails); return ( { const dispatch = useDispatch(); const store = useStore(); const toast = useIOToast(); - const paymentId = getRptIdStringFromPayment(payment); - const globalState = store.getState() as GlobalState; const shouldUpdatePayment = shouldUpdatePaymentSelector( globalState, messageId, - paymentId + rptId ); const paymentStatusForUI = useIOSelector(state => - paymentStatusForUISelector(state, messageId, paymentId) + paymentStatusForUISelector(state, messageId, rptId) + ); + + const canNavigateToPayment = useIOSelector(state => + canNavigateToPaymentFromMessageSelector(state) ); const startPaymentCallback = useCallback(() => { initializeAndNavigateToWalletForPayment( - paymentId, + messageId, + rptId, + isError(paymentStatusForUI), + paymentAmount, + canNavigateToPayment, dispatch, + isPNPayment, () => toast.error(I18n.t("genericError")), () => willNavigateToPayment?.() ); - }, [dispatch, paymentId, toast, willNavigateToPayment]); + }, [ + canNavigateToPayment, + dispatch, + isPNPayment, + messageId, + paymentAmount, + paymentStatusForUI, + rptId, + toast, + willNavigateToPayment + ]); useEffect(() => { if (shouldUpdatePayment) { const updateAction = updatePaymentForMessage.request({ messageId, - paymentId + paymentId: rptId }); dispatch(updateAction); } - }, [dispatch, messageId, paymentId, shouldUpdatePayment]); + }, [dispatch, messageId, rptId, shouldUpdatePayment]); return ( {!noSpaceOnTop && 0 ? 8 : 24} />} {modulePaymentNoticeFromPaymentStatus( - payment.noticeCode, + hideExpirationDate, + noticeNumber, paymentStatusForUI, startPaymentCallback )} diff --git a/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx b/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx index 43537c53909..64d1ecf7a82 100644 --- a/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx +++ b/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx @@ -10,7 +10,7 @@ import { export type OrganizationHeaderProps = { organizationName: string; serviceName: string; - logoUri: ImageURISource; + logoUri: ReadonlyArray; }; const ITEM_PADDING_VERTICAL: IOSpacingScale = 6; diff --git a/ts/features/messages/components/MessageDetail/ShowMoreListItem.tsx b/ts/features/messages/components/MessageDetail/ShowMoreListItem.tsx new file mode 100644 index 00000000000..288b19ba796 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/ShowMoreListItem.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { ListItemAction, ListItemInfoCopy } from "@pagopa/io-app-design-system"; +import I18n from "../../../../i18n"; +import { UIMessageId } from "../../types"; +import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; +import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard"; + +type ShowMoreListItemProps = { + messageId: UIMessageId; +}; + +export const ShowMoreListItem = ({ messageId }: ShowMoreListItemProps) => { + const { bottomSheet, present } = useIOBottomSheetAutoresizableModal( + { + component: ( + clipboardSetStringWithFeedback(messageId)} + /> + ), + title: I18n.t("messageDetails.showMoreDataBottomSheet.title") + }, + 100 + ); + + return ( + <> + + {bottomSheet} + + ); +}; diff --git a/ts/features/messages/components/__test__/LegacyMessageAttachments.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/LegacyMessageAttachments.test.tsx similarity index 80% rename from ts/features/messages/components/__test__/LegacyMessageAttachments.test.tsx rename to ts/features/messages/components/MessageDetail/__tests__/LegacyMessageAttachments.test.tsx index 52eb7e44539..d59b81cc351 100644 --- a/ts/features/messages/components/__test__/LegacyMessageAttachments.test.tsx +++ b/ts/features/messages/components/MessageDetail/__tests__/LegacyMessageAttachments.test.tsx @@ -2,20 +2,21 @@ import React from "react"; import { act } from "@testing-library/react-native"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { createStore } from "redux"; -import { applicationChangeState } from "../../../../store/actions/application"; -import { appReducer } from "../../../../store/reducers"; -import { GlobalState } from "../../../../store/reducers/types"; -import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; import { LegacyMessageAttachments } from "../LegacyMessageAttachments"; -import { Downloads } from "../../store/reducers/downloads"; -import { mockPdfAttachment } from "../../__mocks__/attachment"; -import { downloadAttachment } from "../../store/actions"; -import { MESSAGES_ROUTES } from "../../navigation/routes"; +import { Downloads } from "../../../store/reducers/downloads"; +import { mockPdfAttachment } from "../../../__mocks__/attachment"; +import { downloadAttachment } from "../../../store/actions"; +import { MESSAGES_ROUTES } from "../../../navigation/routes"; +import { messageId_1 } from "../../../__mocks__/messages"; const mockOpenPreview = jest.fn(); const mockShowToast = jest.fn(); -jest.mock("../../../../utils/showToast", () => ({ +jest.mock("../../../../../utils/showToast", () => ({ showToast: () => mockShowToast() })); @@ -35,10 +36,11 @@ describe("LegacyMessageAttachments", () => { const { component } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: loadingPot } } @@ -64,10 +66,11 @@ describe("LegacyMessageAttachments", () => { const { component } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: notLoadingPot } } @@ -84,10 +87,11 @@ describe("LegacyMessageAttachments", () => { const { store } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.noneLoading } } @@ -97,6 +101,7 @@ describe("LegacyMessageAttachments", () => { store.dispatch( downloadAttachment.failure({ attachment: mockPdfAttachment, + messageId: messageId_1, error: new Error() }) ) @@ -110,10 +115,11 @@ describe("LegacyMessageAttachments", () => { const { store } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: mockOpenPreview() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.noneLoading } } @@ -123,6 +129,7 @@ describe("LegacyMessageAttachments", () => { store.dispatch( downloadAttachment.success({ path: "path", + messageId: messageId_1, attachment: mockPdfAttachment }) ) diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailHeader.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailHeader.test.tsx deleted file mode 100644 index 145c99ef552..00000000000 --- a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailHeader.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { ComponentProps } from "react"; -import { render } from "@testing-library/react-native"; -import { MessageDetailHeader } from "../MessageDetailHeader"; -import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; - -const service = { - service_id: "serviceId", - service_name: "health", - organization_name: "Organization foo", - department_name: "Department one", - organization_fiscal_code: "OFSAAAAAA" -} as ServicePublic; - -const defaultProps: ComponentProps = { - subject: "Subject", - createdAt: new Date("2021-10-18T16:00:30.541Z") -}; - -describe("MessageDetailHeader component", () => { - it("should match the snapshot with default props", () => { - const component = render(); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it("should match the snapshot with all props", () => { - const component = render( - - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it("should render the organization info when the service is defined", () => { - const component = render( - - ); - expect(component.queryByText(service.organization_name)).not.toBeNull(); - expect(component.queryByText(service.service_name)).not.toBeNull(); - }); -}); diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachmentItem.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachmentItem.test.tsx new file mode 100644 index 00000000000..d41de060dc9 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachmentItem.test.tsx @@ -0,0 +1,124 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../../store/reducers"; +import { downloadAttachment } from "../../../store/actions"; +import { UIMessageId } from "../../../types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MessageDetailsAttachmentItem } from "../MessageDetailsAttachmentItem"; +import { ThirdPartyAttachment } from "../../../../../../definitions/backend/ThirdPartyAttachment"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; + +describe("MessageDetailsAttachmentItem", () => { + it("Should match snapshot with required parameters", () => { + const messageId = "01HNWXJG52YS359GWSYSRK2BWC" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url", + content_type: "application/pdf", + name: "A PDF File", + category: "DOCUMENT" + } as ThirdPartyAttachment; + + const component = renderScreen(thirdPartyAttachment, messageId); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with all parameters", () => { + const messageId = "01HNWXJG52YS359GWSYSRK2BWC" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url", + content_type: "application/pdf", + name: "A PDF File", + category: "DOCUMENT" + } as ThirdPartyAttachment; + const serviceId = "01HNWXKWAGWPHV7VGMQ21EZPSA" as ServiceId; + + const component = renderScreen( + thirdPartyAttachment, + messageId, + serviceId, + true, + false, + true + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot when the attachment has no name", () => { + const messageId = "01HNWXJG52YS359GWSYSRK2BWC" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url", + content_type: "application/pdf", + category: "DOCUMENT" + } as ThirdPartyAttachment; + + const component = renderScreen(thirdPartyAttachment, messageId); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot when is fetching the attachment", () => { + const messageId = "01HNWXJG52YS359GWSYSRK2BWC" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url", + content_type: "application/pdf", + category: "DOCUMENT" + } as ThirdPartyAttachment; + + const component = renderScreen( + thirdPartyAttachment, + messageId, + undefined, + undefined, + true, + false + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = ( + attachment: ThirdPartyAttachment, + messageId: UIMessageId, + serviceId?: ServiceId, + bottomSpacer?: boolean, + isFetching?: boolean, + disabled?: boolean +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const finalState = appReducer( + designSystemState, + isFetching + ? downloadAttachment.request({ + attachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + : downloadAttachment.success({ + messageId, + attachment, + path: "file:///fileName.pdf" + }) + ); + const store = createStore(appReducer, finalState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachments.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachments.test.tsx new file mode 100644 index 00000000000..7865045300b --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsAttachments.test.tsx @@ -0,0 +1,106 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../../store/reducers"; +import { UIMessageId } from "../../../types"; +import { MessageDetailsAttachments } from "../MessageDetailsAttachments"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { loadThirdPartyMessage } from "../../../store/actions"; +import { ThirdPartyMessage } from "../../../../../../definitions/backend/ThirdPartyMessage"; +import { ThirdPartyMessageWithContent } from "../../../../../../definitions/backend/ThirdPartyMessageWithContent"; +import { ThirdPartyAttachment } from "../../../../../../definitions/backend/ThirdPartyAttachment"; +import { ATTACHMENT_CATEGORY } from "../../../types/attachmentCategory"; + +describe("MessageDetailsAttachments", () => { + it("Should match snapshot with no attachments", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with no attachments and disabled UI", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 0, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with no attachments, where F24 have been removed and disabled UI", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 0, true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 1 attachment", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 1); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 1 attachment that is disabled", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 1, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 1 attachment that is disabled and F24 have been removed", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 1, true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 10 attachments", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 10); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 10 attachments that are disabled", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 10, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with 5 attachments that are disabled and F24 have been removed", () => { + const messageId = "01HNWYRT55GXGPXR16BW2MSBVY" as UIMessageId; + const component = renderScreen(messageId, 10, true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = ( + messageId: UIMessageId, + attachmentCount: number = 0, + disabled: boolean = false, + isPN: boolean = false +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + + const attachments = Array(attachmentCount).map(index => ({ + id: `${index}`, + url: `https://invalid.url/${index}.pdf`, + category: index % 2 === 1 ? ATTACHMENT_CATEGORY.F24 : undefined + })) as Array; + + const finalState = appReducer( + designSystemState, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: { + attachments + } as ThirdPartyMessage + } as ThirdPartyMessageWithContent + }) + ); + const store = createStore(appReducer, finalState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsFooter.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsFooter.test.tsx new file mode 100644 index 00000000000..df4593179ee --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsFooter.test.tsx @@ -0,0 +1,132 @@ +import React, { ComponentProps } from "react"; +import { Action, Store } from "redux"; +import configureMockStore from "redux-mock-store"; +import { fireEvent } from "@testing-library/react-native"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { loadServiceDetail } from "../../../../../store/actions/services"; +import { messageId_1, service_1 } from "../../../__mocks__/messages"; +import { reproduceSequence } from "../../../../../utils/tests"; +import { MessageDetailsFooter } from "../MessageDetailsFooter"; +import { ServiceMetadata } from "../../../../../../definitions/backend/ServiceMetadata"; + +const mockPresentBottomSheet = jest.fn(); + +jest.mock("../../../../../utils/hooks/bottomSheet", () => ({ + useIOBottomSheetAutoresizableModal: () => ({ + present: mockPresentBottomSheet + }) +})); + +const defaultProps: ComponentProps = { + messageId: messageId_1, + serviceId: service_1.service_id +}; + +describe("MessageDetailsFooter component", () => { + beforeEach(() => { + mockPresentBottomSheet.mockReset(); + }); + + it("should match the snapshot when the service's contact details are defined", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success({ + ...service_1, + service_metadata: { + email: "test@test.com", + phone: "+393331234567" + } as ServiceMetadata + }) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const { component } = renderComponent(state, defaultProps); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("should match the snapshot when the service's contact details are not defined", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success(service_1) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const { component } = renderComponent(state, defaultProps); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("should call present function when the 'Show more data' action is pressed", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success(service_1) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const { component } = renderComponent(state, defaultProps); + + const showMoreDataAction = component.getByTestId("show-more-data-action"); + fireEvent.press(showMoreDataAction); + expect(mockPresentBottomSheet).toBeCalledTimes(1); + }); + + it("should call present function when the 'Contacts' action is pressed", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success({ + ...service_1, + service_metadata: { + email: "test@test.com", + phone: "+393331234567" + } as ServiceMetadata + }) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const { component } = renderComponent(state, defaultProps); + + const contactsAction = component.getByTestId("contacts-action"); + fireEvent.press(contactsAction); + expect(mockPresentBottomSheet).toBeCalledTimes(1); + }); +}); + +const renderComponent = ( + state: GlobalState, + props: React.ComponentProps +) => { + const mockStore = configureMockStore(); + const store: Store = mockStore(state); + + return { + component: renderScreenWithNavigationStoreContext( + () => , + "DUMMY_ROUTE", + {}, + store + ), + store + }; +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsHeader.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsHeader.test.tsx new file mode 100644 index 00000000000..43f6ae34900 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsHeader.test.tsx @@ -0,0 +1,61 @@ +import React, { ComponentProps } from "react"; +import { Action, Store } from "redux"; +import configureMockStore from "redux-mock-store"; +import { MessageDetailsHeader } from "../MessageDetailsHeader"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { loadServiceDetail } from "../../../../../store/actions/services"; +import { service_1 } from "../../../__mocks__/messages"; +import { reproduceSequence } from "../../../../../utils/tests"; + +const defaultProps: ComponentProps = { + createdAt: new Date("2021-10-18T16:00:30.541Z"), + serviceId: service_1.service_id, + subject: "#### Subject ####" +}; + +describe("MessageDetailsHeader component", () => { + it("should match the snapshot with default props", () => { + const { component } = renderComponent(defaultProps); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("should NOT render the organization info when the serviceId is invalid", () => { + const { component } = renderComponent({ + ...defaultProps, + serviceId: "invalid" as ServiceId + }); + expect(component.queryByText(service_1.organization_name)).toBeNull(); + expect(component.queryByText(service_1.service_name)).toBeNull(); + }); +}); + +const renderComponent = ( + props: React.ComponentProps +) => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success(service_1) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const mockStore = configureMockStore(); + const store: Store = mockStore(state); + + return { + component: renderScreenWithNavigationStoreContext( + () => , + "DUMMY_ROUTE", + {}, + store + ), + store + }; +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPayment.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPayment.test.tsx new file mode 100644 index 00000000000..036a877bd2a --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPayment.test.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../../store/reducers"; +import { PaymentData, UIMessageDetails, UIMessageId } from "../../../types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MessageDetailsPayment } from "../MessageDetailsPayment"; +import { loadMessageDetails } from "../../../store/actions"; + +describe("MessageDetailsPayment", () => { + it("Should match snapshot for no payment data", () => { + const messageId = "01HRA9THAS0NHCXS20A0FAW5Y2" as UIMessageId; + const component = renderScreen(messageId); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot when there are payment data", () => { + const messageId = "01HRA9TP2GMNHGMF2GK8V4PYSF" as UIMessageId; + const component = renderScreen(messageId, { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + }, + amount: 199 + } as PaymentData); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = (messageId: UIMessageId, paymentData?: PaymentData) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const finalState = appReducer( + designSystemState, + loadMessageDetails.success({ + id: messageId, + paymentData + } as UIMessageDetails) + ); + const store = createStore(appReducer, finalState as any); + + return renderScreenWithNavigationStoreContext( + () => , + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx new file mode 100644 index 00000000000..7a1c32f65c9 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx @@ -0,0 +1,50 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../../store/reducers"; +import { MessageDetailsPaymentButton } from "../MessageDetailsPaymentButton"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { PaymentData, UIMessageId } from "../../../types"; + +describe("MessageDetailsPaymentButton", () => { + it("should match snapshot when not loading", () => { + const screen = renderScreen(false); + expect(screen.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when loading", () => { + const screen = renderScreen(true); + expect(screen.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = (isLoading: boolean) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminder.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminder.test.tsx new file mode 100644 index 00000000000..45c49a7052b --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminder.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { createStore } from "redux"; +import { MessageDetailsReminder } from "../MessageDetailsReminder"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { UIMessageId } from "../../../types"; +import * as payments from "../../../store/reducers/payments"; + +const dueDate = new Date(2024, 2, 21, 18, 44, 31); + +describe("MessageDetailsReminder", () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + it("should match snapshot when the reminder is hidden", () => { + jest + .spyOn(payments, "paymentExpirationBannerStateSelector") + .mockImplementation((_state, _messageId) => "hidden"); + const component = renderScreen(dueDate); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when the due date is missing", () => { + jest + .spyOn(payments, "paymentExpirationBannerStateSelector") + .mockImplementation((_state, _messageId) => "visibleExpired"); + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when the reminder is loading", () => { + jest + .spyOn(payments, "paymentExpirationBannerStateSelector") + .mockImplementation((_state, _messageId) => "loading"); + const component = renderScreen(dueDate); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when the reminder is visible an expiring", () => { + jest + .spyOn(payments, "paymentExpirationBannerStateSelector") + .mockImplementation((_state, _messageId) => "visibleExpiring"); + const component = renderScreen(dueDate); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when the reminder is visible and expired", () => { + jest + .spyOn(payments, "paymentExpirationBannerStateSelector") + .mockImplementation((_state, _messageId) => "visibleExpired"); + const component = renderScreen(dueDate); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = (dueDate?: Date) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpired.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpired.test.tsx new file mode 100644 index 00000000000..e7c69f1f7ae --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpired.test.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { createStore } from "redux"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MessageDetailsReminderExpired } from "../MessageDetailsReminderExpired"; + +describe("MessageDetailsReminderExpired", () => { + it("should match snapshot when loading", () => { + const component = renderScreen(true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when not loading", () => { + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = (isLoading: boolean = false) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpiring.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpiring.test.tsx new file mode 100644 index 00000000000..c3325d9abef --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsReminderExpiring.test.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { createStore } from "redux"; +import { MessageDetailsReminderExpiring } from "../MessageDetailsReminderExpiring"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { UIMessageId } from "../../../types"; + +describe("MessageDetailsReminderExpiring", () => { + it("should match snapshot", () => { + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = () => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx new file mode 100644 index 00000000000..bc1d8571115 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MessageDetailsScrollViewAdditionalSpace } from "../MessageDetailsScrollViewAdditionalSpace"; +import { UIMessageId } from "../../../types"; +import * as payments from "../../../store/reducers/payments"; + +describe("MessageDetailsScrollViewAdditionalSpace", () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + it("Should match snapshot with hidden button and no CTAs", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(false); + const component = renderComponent(false, false); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with hidden button and both CTAs", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(false); + const component = renderComponent(true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with hidden button and a single CTA", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(false); + const component = renderComponent(true, false); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with button and no CTAs", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(true); + const component = renderComponent(false, false); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with button and both CTA", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(true); + const component = renderComponent(true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with button and a single CTA", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(true); + const component = renderComponent(true, false); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = ( + hasCTA1: boolean, + hasCTA2: boolean, + messageId: UIMessageId = "01HRW5J2QYMH3FWAA5CYGXSC84" as UIMessageId +) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, globalState as any); + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx new file mode 100644 index 00000000000..0e8103073f8 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MessageDetailsStickyFooter } from "../MessageDetailsStickyFooter"; +import { PaymentData, UIMessageId } from "../../../types"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { CTA, CTAS } from "../../../types/MessageCTA"; +import * as detailsById from "../../../store/reducers/detailsById"; +import * as payments from "../../../store/reducers/payments"; +import { PaymentAmount } from "../../../../../../definitions/backend/PaymentAmount"; +import { PaymentNoticeNumber } from "../../../../../../definitions/backend/PaymentNoticeNumber"; +import { OrganizationFiscalCode } from "../../../../../../definitions/backend/OrganizationFiscalCode"; + +const cta1: CTA = { + text: "CTA 1", + action: "" +}; +const cta2: CTA = { + text: "CTA 2", + action: "" +}; +const bothCTAs: CTAS = { + cta_1: cta1, + cta_2: cta2 +}; +const onlyCTA1: CTAS = { + cta_1: cta1 +}; +const paymentData: PaymentData = { + amount: 199 as PaymentAmount, + noticeNumber: "012345678912345610" as PaymentNoticeNumber, + payee: { + fiscalCode: "01234567890" as OrganizationFiscalCode + } +}; + +describe("MessageDetailsStickyFooter", () => { + it("should match snapshot with both CTAs and visible payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => paymentData); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "enabled"); + const component = renderComponent(bothCTAs); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with one CTA and visible payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => paymentData); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "enabled"); + const component = renderComponent(onlyCTA1); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with no CTAs and enabled payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => paymentData); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "enabled"); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with no CTAs and loading payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => paymentData); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "loading"); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with no CTAs and hidden payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => paymentData); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "hidden"); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with both CTAs and no payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => undefined); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "hidden"); + const component = renderComponent(bothCTAs); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with one CTA and no payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => undefined); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "hidden"); + const component = renderComponent(onlyCTA1); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot with no CTA and no payment button", () => { + jest + .spyOn(detailsById, "messagePaymentDataSelector") + .mockImplementation((_state, _messageId) => undefined); + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockImplementation((_state, _messageId) => "hidden"); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = (ctas?: CTAS) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, globalState as any); + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/MessagePaymentItem.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessagePaymentItem.test.tsx similarity index 74% rename from ts/features/pn/components/__test__/MessagePaymentItem.test.tsx rename to ts/features/messages/components/MessageDetail/__tests__/MessagePaymentItem.test.tsx index a68779711e0..334c167177f 100644 --- a/ts/features/pn/components/__test__/MessagePaymentItem.test.tsx +++ b/ts/features/messages/components/MessageDetail/__tests__/MessagePaymentItem.test.tsx @@ -1,14 +1,14 @@ import React from "react"; import { createStore } from "redux"; -import { applicationChangeState } from "../../../../store/actions/application"; -import { appReducer } from "../../../../store/reducers"; -import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; import { MessagePaymentItem } from "../MessagePaymentItem"; -import { UIMessageId } from "../../../messages/types"; -import { NotificationPaymentInfo } from "../../../../../definitions/pn/NotificationPaymentInfo"; -import { updatePaymentForMessage } from "../../store/actions"; -import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; -import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { UIMessageId } from "../../../types"; +import { NotificationPaymentInfo } from "../../../../../../definitions/pn/NotificationPaymentInfo"; +import { Detail_v2Enum } from "../../../../../../definitions/backend/PaymentProblemJson"; +import { updatePaymentForMessage } from "../../../store/actions"; +import { PaymentRequestsGetResponse } from "../../../../../../definitions/backend/PaymentRequestsGetResponse"; describe("MessagePaymentItem component", () => { it("Should match the snapshot for a loading item", () => { @@ -53,6 +53,7 @@ const renderComponent = ( payment: NotificationPaymentInfo, paymentStatus: "payable" | "processed" | undefined = undefined ) => { + const rptId = `${payment.creditorTaxId}${payment.noticeCode}`; const globalState = appReducer(undefined, applicationChangeState("active")); const modifiedState = paymentStatus === "payable" @@ -60,7 +61,7 @@ const renderComponent = ( globalState, updatePaymentForMessage.success({ messageId, - paymentId: `${payment.creditorTaxId}${payment.noticeCode}`, + paymentId: rptId, paymentData: { codiceContestoPagamento: `${payment.noticeCode}`, importoSingoloVersamento: 99, @@ -74,7 +75,7 @@ const renderComponent = ( globalState, updatePaymentForMessage.failure({ messageId, - paymentId: `${payment.creditorTaxId}${payment.noticeCode}`, + paymentId: rptId, details: Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO }) ) @@ -83,7 +84,12 @@ const renderComponent = ( return renderScreenWithNavigationStoreContext( () => ( - + ), "DUMMY", {}, diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/CtaBar.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/CtaBar.test.tsx.snap index 4ddc122e826..aea147a5e16 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/CtaBar.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/CtaBar.test.tsx.snap @@ -20,291 +20,330 @@ exports[`the \`CtaBar\` component when neither \`paymentData\` nor \`dueDate\` a } > - - - + + /> + + - + - MESSAGE_DETAIL - + + MESSAGE_DETAIL + + + - - - - - + - + style={ + Object { + "flex": 1, + } + } + > + + - - + + diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/DueDate.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/DueDate.test.tsx.snap index 0c47369b3f7..f31b577e378 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/DueDate.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/DueDate.test.tsx.snap @@ -44,13 +44,31 @@ Array [ vbWidth={40} width={40} > - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - Subject - - - - 18 Oct 2021, 16:00 - - - - - - - Organization foo - - - health - - - - - - - - - - -`; - -exports[`MessageDetailHeader component should match the snapshot with default props 1`] = ` - - - Subject - - - - 18 Oct 2021, 16:00 - - - - -`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachmentItem.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachmentItem.test.tsx.snap new file mode 100644 index 00000000000..dc8ddf292f9 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachmentItem.test.tsx.snap @@ -0,0 +1,2393 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsAttachmentItem Should match snapshot when is fetching the attachment 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachmentItem Should match snapshot when the attachment has no name 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachmentItem Should match snapshot with all parameters 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + A PDF File + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachmentItem Should match snapshot with required parameters 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + A PDF File + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachments.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachments.test.tsx.snap new file mode 100644 index 00000000000..47cf045e1b5 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsAttachments.test.tsx.snap @@ -0,0 +1,3638 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsAttachments Should match snapshot with 1 attachment 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with 1 attachment that is disabled 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with 1 attachment that is disabled and F24 have been removed 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with 5 attachments that are disabled and F24 have been removed 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with 10 attachments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with 10 attachments that are disabled 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with no attachments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with no attachments and disabled UI 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsAttachments Should match snapshot with no attachments, where F24 have been removed and disabled UI 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsFooter.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsFooter.test.tsx.snap new file mode 100644 index 00000000000..d03588a14bc --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsFooter.test.tsx.snap @@ -0,0 +1,1181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsFooter component should match the snapshot when the service's contact details are defined 1`] = ` + + + + + + + + + + + + + + + DUMMY_ROUTE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contact the sender + + + + + + + + + + + + + + + + + + Show more data + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsFooter component should match the snapshot when the service's contact details are not defined 1`] = ` + + + + + + + + + + + + + + + DUMMY_ROUTE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show more data + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsHeader.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsHeader.test.tsx.snap new file mode 100644 index 00000000000..71dc6da011c --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsHeader.test.tsx.snap @@ -0,0 +1,598 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsHeader component should match the snapshot with default props 1`] = ` + + + + + + + + + + + + + + + DUMMY_ROUTE + + + + + + + + + + + + + + + + + + + + #### Subject #### + + + + 18 Oct 2021, 16:00 + + + + + + + Ċentru tas-Saħħa + + + health + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPayment.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPayment.test.tsx.snap new file mode 100644 index 00000000000..a7dbf9819b3 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPayment.test.tsx.snap @@ -0,0 +1,953 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsPayment Should match snapshot for no payment data 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsPayment Should match snapshot when there are payment data 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pagoPA notices + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap new file mode 100644 index 00000000000..fc234e53d50 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap @@ -0,0 +1,1047 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsPaymentButton should match snapshot when loading 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsPaymentButton should match snapshot when not loading 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + Pay + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminder.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminder.test.tsx.snap new file mode 100644 index 00000000000..967b238968e --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminder.test.tsx.snap @@ -0,0 +1,2084 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsReminder should match snapshot when the due date is missing 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsReminder should match snapshot when the reminder is hidden 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsReminder should match snapshot when the reminder is loading 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsReminder should match snapshot when the reminder is visible an expiring 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + Expires on 21/03/2024 at 18:44 + + + + Add reminder + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsReminder should match snapshot when the reminder is visible and expired 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + Expired on 21 Mar at 18:44 + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpired.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpired.test.tsx.snap new file mode 100644 index 00000000000..d1f0348020c --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpired.test.tsx.snap @@ -0,0 +1,850 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsReminderExpired should match snapshot when loading 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsReminderExpired should match snapshot when not loading 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + Expired on 21 Mar at 10:33 + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpiring.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpiring.test.tsx.snap new file mode 100644 index 00000000000..5f62ac19f2f --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsReminderExpiring.test.tsx.snap @@ -0,0 +1,524 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsReminderExpiring should match snapshot 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + Expires on 21/03/2024 at 10:33 + + + + Add reminder + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap new file mode 100644 index 00000000000..8a2ce201b8b --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap @@ -0,0 +1,2119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and a single CTA 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and both CTA 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and no CTAs 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and a single CTA 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and both CTAs 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and no CTAs 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap new file mode 100644 index 00000000000..0bdb8ab1187 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap @@ -0,0 +1,4138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and no payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + CTA 1 + + + + + + + + + + CTA 2 + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and visible payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + Pay + + + + + + + + + CTA 1 + + + + + + + + + CTA 2 + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with no CTA and no payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with no CTAs and enabled payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + Pay + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with no CTAs and hidden payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with no CTAs and loading payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with one CTA and no payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + CTA 1 + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsStickyFooter should match snapshot with one CTA and visible payment button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + Pay + + + + + + + + + + CTA 1 + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessagePaymentItem.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessagePaymentItem.test.tsx.snap new file mode 100644 index 00000000000..83396698630 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessagePaymentItem.test.tsx.snap @@ -0,0 +1,1632 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessagePaymentItem component Should match the snapshot for a loading item 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePaymentItem component Should match the snapshot for a payable item 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + Due date 23/11/2023 + + + Causale + + + + + 0.99 € + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePaymentItem component Should match the snapshot for a processed item 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + Notice code + + + n1 + + + + + + Revoked + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/OrganizationHeader.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/OrganizationHeader.test.tsx.snap index 8ccc1da6e7c..980f17f7fd2 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/OrganizationHeader.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/OrganizationHeader.test.tsx.snap @@ -89,17 +89,18 @@ exports[`OrganizationHeader component should match the snapshot 1`] = ` } > - + > + + diff --git a/ts/features/messages/components/MessageDetail/index.tsx b/ts/features/messages/components/MessageDetail/index.tsx index 245dc7c0bad..ea1c19bb098 100644 --- a/ts/features/messages/components/MessageDetail/index.tsx +++ b/ts/features/messages/components/MessageDetail/index.tsx @@ -16,16 +16,13 @@ import I18n from "../../../../i18n"; import { OrganizationFiscalCode } from "../../../../../definitions/backend/OrganizationFiscalCode"; import { ServiceMetadata } from "../../../../../definitions/backend/ServiceMetadata"; import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; -import { LegacyMessageAttachments } from "../LegacyMessageAttachments"; import { useIOSelector } from "../../../../store/hooks"; import { messageMarkdownSelector, messageTitleSelector, thirdPartyFromIdSelector } from "../../store/reducers/thirdPartyById"; - -import { UIAttachment, UIMessage, UIMessageDetails } from "../../types"; -import { attachmentsFromThirdPartyMessage } from "../../store/reducers/transformers"; +import { UIMessage, UIMessageDetails, UIMessageId } from "../../types"; import { UIService } from "../../../../store/reducers/entities/services/types"; import variables from "../../../../theme/variables"; import { cleanMarkdownFromCTAs } from "../../utils/messages"; @@ -38,11 +35,13 @@ import { } from "../../../../navigation/params/AppParamsList"; import StatusContent from "../../../../components/SectionStatus/StatusContent"; import { MESSAGES_ROUTES } from "../../navigation/routes"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { LegacyMessageAttachments } from "./LegacyMessageAttachments"; import CtaBar from "./CtaBar"; import { RemoteContentBanner } from "./RemoteContentBanner"; import { HeaderDueDateBar } from "./HeaderDueDateBar"; import MessageContent from "./Content"; -import MessageMarkdown from "./MessageMarkdown"; +import { MessageMarkdown } from "./MessageMarkdown"; const styles = StyleSheet.create({ webview: { @@ -157,14 +156,13 @@ const MessageDetailsComponent = ({ const serviceIdOpt = service?.id; const openAttachment = useCallback( - (attachment: UIAttachment) => { + (attachment: ThirdPartyAttachment) => { navigation.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { screen: MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT, params: { messageId, serviceId: serviceIdOpt, - attachmentId: attachment.id, - isPN: false + attachmentId: attachment.id } }); }, @@ -172,18 +170,21 @@ const MessageDetailsComponent = ({ ); const renderThirdPartyAttachments = useCallback( - (thirdPartyMessage: ThirdPartyMessageWithContent): React.ReactNode => { + ( + messageId: UIMessageId, + thirdPartyMessage: ThirdPartyMessageWithContent + ): React.ReactNode => { // In order not to break or refactor existing PN code, the backend // model for third party attachments is converted into in-app // model for attachments when the user generates the request. This // is not a speed intensive operation nor a memory consuming task, // since the attachment count should be negligible - const maybeThirdPartyMessageAttachments = - attachmentsFromThirdPartyMessage(thirdPartyMessage); - return O.isSome(maybeThirdPartyMessageAttachments) ? ( + const attachmentsOpt = thirdPartyMessage.third_party_message.attachments; + return attachmentsOpt ? ( @@ -244,7 +245,7 @@ const MessageDetailsComponent = ({ _ => renderThirdPartyAttachmentsLoading(), _ => renderThirdPartyAttachmentsError(), thirdPartyMessage => - renderThirdPartyAttachments(thirdPartyMessage), + renderThirdPartyAttachments(messageId, thirdPartyMessage), _ => renderThirdPartyAttachmentsLoading(), _ => renderThirdPartyAttachmentsLoading(), _ => renderThirdPartyAttachmentsError() diff --git a/ts/features/messages/components/MessageList/__tests__/__snapshots__/Item.test.tsx.snap b/ts/features/messages/components/MessageList/__tests__/__snapshots__/Item.test.tsx.snap index 1718022c537..899338b3e38 100644 --- a/ts/features/messages/components/MessageList/__tests__/__snapshots__/Item.test.tsx.snap +++ b/ts/features/messages/components/MessageList/__tests__/__snapshots__/Item.test.tsx.snap @@ -471,11 +471,23 @@ exports[`MessageList Item component when \`isSelectionModeEnabled\` is true and } width={10} > - + - + - + - + - + - + - + - + - + - + - + - + - () => { + (): EmptyComponent => { if (error !== undefined) { return ; } if (EmptyComponent) { - return ; + return EmptyComponent; } return null; }; diff --git a/ts/features/messages/components/MessageList/index.tsx b/ts/features/messages/components/MessageList/index.tsx index 81c1b93d90e..19d1b6c2ce0 100644 --- a/ts/features/messages/components/MessageList/index.tsx +++ b/ts/features/messages/components/MessageList/index.tsx @@ -12,7 +12,7 @@ import { Vibration } from "react-native"; import { connect } from "react-redux"; - +import { useFocusEffect } from "@react-navigation/native"; import { maximumItemsFromAPI, pageSize } from "../../../../config"; import { useTabItemPressWhenScreenActive } from "../../../../hooks/useTabItemPressWhenScreenActive"; import I18n from "../../../../i18n"; @@ -39,7 +39,6 @@ import customVariables, { VIBRATION_LONG_PRESS_DURATION } from "../../../../theme/variables"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { useActionOnFocus } from "../../../../utils/hooks/useOnFocus"; import { showToast } from "../../../../utils/showToast"; import { EmptyComponent, @@ -156,6 +155,7 @@ const MessageList = ({ O.Option >(O.none); + const [lastUpdate, setLastUpdate] = useState(new Date()); const [isRefreshFromUser, setIsRefreshFromUser] = useState(false); const isLoadingPreviousOrAll = isLoadingPrevious || isReloadingAll; @@ -167,26 +167,6 @@ const MessageList = ({ useTabItemPressWhenScreenActive(() => scrollTo(0, true), true); - useOnFirstRender( - () => { - reloadAll(); - }, - () => shouldUseLoad && !didLoad - ); - - useActionOnFocus(() => { - // check if there are new messages when the component becomes focused - if (previousCursor) { - loadPreviousPage(previousCursor); - } - }, minimumRefreshInterval); - - useEffect(() => { - if (error) { - showToast(I18n.t("global.genericError"), "warning"); - } - }, [error]); - const hasMessages = messages.length > 0; const scrollTo = (index: number, animated: boolean = false) => { if (flatListRef.current && hasMessages) { @@ -256,6 +236,39 @@ const MessageList = ({ const isLoadingOrRefreshingMessageList = isLoadingMore || isLoadingPreviousOrAll; + useOnFirstRender( + () => { + reloadAll(); + }, + () => shouldUseLoad && !didLoad + ); + + // check if there are new messages when the component becomes focused + useFocusEffect( + useCallback(() => { + const now = new Date(); + const shouldRefreshDelay = + now.getTime() - lastUpdate.getTime() > minimumRefreshInterval; + if (shouldRefreshDelay && previousCursor) { + if (!isLoadingOrRefreshingMessageList) { + loadPreviousPage(previousCursor); + } + setLastUpdate(now); + } + }, [ + isLoadingOrRefreshingMessageList, + lastUpdate, + loadPreviousPage, + previousCursor + ]) + ); + + useEffect(() => { + if (error) { + showToast(I18n.t("global.genericError"), "warning"); + } + }, [error]); + const refreshControl = shouldUseLoad ? ( message.id} @@ -314,7 +327,7 @@ const MessageList = ({ onEndReached={onEndReached} onEndReachedThreshold={0.25} testID={testID} - ListFooterComponent={shouldShowFooterLoader && } + ListFooterComponent={shouldShowFooterLoader ? : null} /> ); diff --git a/ts/features/messages/components/MessageLoading.tsx b/ts/features/messages/components/MessageLoading.tsx deleted file mode 100644 index 5f14544923d..00000000000 --- a/ts/features/messages/components/MessageLoading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { ActivityIndicator } from "react-native"; -import { IOColors } from "@pagopa/io-app-design-system"; -import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { InfoScreenComponent } from "../../../components/infoScreen/InfoScreenComponent"; - -const indicator = ( - -); - -export const MessageLoading = (): React.ReactElement => ( - {I18n.t("features.messages.loading.subtitle")}} - /> -); diff --git a/ts/features/messages/components/PreconditionBottomSheet/PreconditionContent.tsx b/ts/features/messages/components/PreconditionBottomSheet/PreconditionContent.tsx index 96dd89149aa..e3e2651f1c2 100644 --- a/ts/features/messages/components/PreconditionBottomSheet/PreconditionContent.tsx +++ b/ts/features/messages/components/PreconditionBottomSheet/PreconditionContent.tsx @@ -3,7 +3,7 @@ import { View } from "react-native"; import Placeholder from "rn-placeholder"; import { VSpacer } from "@pagopa/io-app-design-system"; import customVariables from "../../../../theme/variables"; -import MessageMarkdown from "../MessageDetail/MessageMarkdown"; +import { MessageMarkdown } from "../MessageDetail/MessageMarkdown"; type Props = { markdown: string; diff --git a/ts/features/messages/components/__test__/__snapshots__/EmptyListComponent.test.tsx.snap b/ts/features/messages/components/__test__/__snapshots__/EmptyListComponent.test.tsx.snap index 15098bd5d92..34d2fe8e1a6 100644 --- a/ts/features/messages/components/__test__/__snapshots__/EmptyListComponent.test.tsx.snap +++ b/ts/features/messages/components/__test__/__snapshots__/EmptyListComponent.test.tsx.snap @@ -65,7 +65,12 @@ exports[`EmptyListComponent matches the snapshot 1`] = ` { mockAndroidAddCompleteDownload.mockImplementation(() => Promise.resolve()); - await taskAddCompleteDownload(mockOtherAttachment, path)(); + const name = attachmentDisplayName(mockOtherAttachment); + const mimeType = attachmentContentType(mockOtherAttachment); + await taskAddCompleteDownload(name, mimeType, path)(); expect(mockAndroidAddCompleteDownload).toBeCalledTimes(1); }); @@ -64,10 +72,9 @@ describe("Open attachment", function () { }); it("Should display an options menu", async () => { - await taskDownloadFileIntoAndroidPublicFolder( - mockOtherAttachment, - path - )(); + const name = attachmentDisplayName(mockOtherAttachment); + const mimeType = attachmentContentType(mockOtherAttachment); + await taskDownloadFileIntoAndroidPublicFolder(name, mimeType, path)(); expect(mockIosPresentOptionsMenu).toBeCalledTimes(1); }); }); @@ -86,10 +93,9 @@ describe("Open attachment", function () { Promise.resolve() ); - await taskDownloadFileIntoAndroidPublicFolder( - mockOtherAttachment, - path - )(); + const name = attachmentDisplayName(mockOtherAttachment); + const mimeType = attachmentContentType(mockOtherAttachment); + await taskDownloadFileIntoAndroidPublicFolder(name, mimeType, path)(); expect(mockMediaCollectionCopyToMediaStore).toBeCalledTimes(1); expect(mockAndroidAddCompleteDownload).toBeCalledTimes(1); expect(mockShowToast).not.toHaveBeenCalled(); @@ -100,10 +106,9 @@ describe("Open attachment", function () { Promise.reject(new Error("Error on reject")) ); - await taskDownloadFileIntoAndroidPublicFolder( - mockOtherAttachment, - path - )(); + const name = attachmentDisplayName(mockOtherAttachment); + const mimeType = attachmentContentType(mockOtherAttachment); + await taskDownloadFileIntoAndroidPublicFolder(name, mimeType, path)(); expect(mockShowToast).toBeCalledTimes(1); }); @@ -115,10 +120,9 @@ describe("Open attachment", function () { Promise.reject(new Error("Error on reject")) ); - await taskDownloadFileIntoAndroidPublicFolder( - mockOtherAttachment, - path - )(); + const name = attachmentDisplayName(mockOtherAttachment); + const mimeType = attachmentContentType(mockOtherAttachment); + await taskDownloadFileIntoAndroidPublicFolder(name, mimeType, path)(); expect(mockShowToast).toBeCalledTimes(1); }); }); diff --git a/ts/features/messages/hooks/useAttachmentDownload.tsx b/ts/features/messages/hooks/useAttachmentDownload.tsx index ad51a2fa9a9..4fc373eff8f 100644 --- a/ts/features/messages/hooks/useAttachmentDownload.tsx +++ b/ts/features/messages/hooks/useAttachmentDownload.tsx @@ -1,186 +1,153 @@ -import { useCallback, useEffect, useState } from "react"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import ReactNativeBlobUtil from "react-native-blob-util"; +import { useCallback, useEffect } from "react"; import RNFS from "react-native-fs"; -import { identity, pipe } from "fp-ts/lib/function"; -import * as E from "fp-ts/lib/Either"; -import * as TE from "fp-ts/lib/TaskEither"; -import i18n from "../../../i18n"; -import { useIODispatch, useIOSelector } from "../../../store/hooks"; -import { ContentTypeValues } from "../types/contentType"; -import { isAndroid } from "../../../utils/platform"; -import { showToast } from "../../../utils/showToast"; +import { IOToast } from "@pagopa/io-app-design-system"; +import { useIODispatch, useIOSelector, useIOStore } from "../../../store/hooks"; +import { + downloadedMessageAttachmentSelector, + hasErrorOccourredOnRequestedDownloadSelector, + isDownloadingMessageAttachmentSelector, + isRequestedAttachmentDownloadSelector +} from "../store/reducers/downloads"; +import { UIMessageId } from "../types"; import { cancelPreviousAttachmentDownload, + clearRequestedAttachmentDownload, downloadAttachment } from "../store/actions"; -import { UIAttachment } from "../types"; -import { downloadPotForMessageAttachmentSelector } from "../store/reducers/downloads"; -import { isTestEnv } from "../../../utils/environment"; -import { trackPNAttachmentDownloadFailure } from "../../pn/analytics"; +import { MESSAGES_ROUTES } from "../navigation/routes"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { attachmentDisplayName } from "../store/reducers/transformers"; +import I18n from "../../../i18n"; +import { + trackPNAttachmentDownloadFailure, + trackPNAttachmentOpening +} from "../../pn/analytics"; import { trackThirdPartyMessageAttachmentShowPreview } from "../analytics"; +import PN_ROUTES from "../../pn/navigation/routes"; +import NavigationService from "../../../navigation/NavigationService"; -const taskCopyToMediaStore = ( - { displayName, contentType }: UIAttachment, - path: string -) => - TE.tryCatch( - () => - ReactNativeBlobUtil.MediaCollection.copyToMediaStore( - { - name: displayName, - parentFolder: "", - mimeType: contentType - }, - "Download", - path - ), - E.toError - ); - -const taskAddCompleteDownload = ( - { displayName, contentType }: UIAttachment, - path: string -) => - TE.tryCatch( - () => - ReactNativeBlobUtil.android.addCompleteDownload({ - mime: contentType, - title: displayName, - showNotification: true, - description: displayName, - path - }), - E.toError - ); - -const taskDownloadFileIntoAndroidPublicFolder = ( - attachment: UIAttachment, - path: string -) => - pipe( - isAndroid, - TE.fromPredicate(identity, () => undefined), - TE.mapLeft(() => ReactNativeBlobUtil.ios.presentOptionsMenu(path)), - TE.chain(_ => - pipe( - taskCopyToMediaStore(attachment, path), - TE.chain(downloadFilePath => - taskAddCompleteDownload(attachment, downloadFilePath) - ), - TE.mapLeft(_ => - showToast(i18n.t("messageDetails.attachments.failing.details")) - ) - ) - ) - ); - -export const testableFunctions = isTestEnv - ? { - taskCopyToMediaStore, - taskAddCompleteDownload, - taskDownloadFileIntoAndroidPublicFolder - } - : undefined; - -// This hook has a different behaviour if the attachment is a PN -// one or a generic third-party attachment. -// When selecting a PN attachment, this hook takes care of downloading -// the attachment before going into the attachment preview component. -// If the attachment is from a third-party message (generic attachment) -// then the download is delegated to another part of the application export const useAttachmentDownload = ( - attachment: UIAttachment, - downloadAttachmentBeforePreview: boolean = false, - openPreview: (attachment: UIAttachment) => void + messageId: UIMessageId, + attachment: ThirdPartyAttachment, + isPN: boolean, + serviceId?: ServiceId, + onPreNavigate?: () => void ) => { - const [isLoading, setIsLoading] = useState(false); + const attachmentId = attachment.id; + const dispatch = useIODispatch(); + const store = useIOStore(); - const downloadPot = useIOSelector(state => - downloadPotForMessageAttachmentSelector(state, attachment) + const download = useIOSelector(state => + downloadedMessageAttachmentSelector(state, messageId, attachmentId) + ); + const isFetching = useIOSelector(state => + isDownloadingMessageAttachmentSelector(state, messageId, attachmentId) ); - const openAttachment = useCallback(async () => { - const download = pot.toUndefined(downloadPot); - - if (pot.isError(downloadPot)) { - trackPNAttachmentDownloadFailure(attachment.category); - showToast(i18n.t("messageDetails.attachments.failing.details")); - } else if (download) { - const { path, attachment } = download; - - if (attachment.contentType === ContentTypeValues.applicationPdf) { - openPreview(attachment); + const attachmentCategory = attachment.category; + const doNavigate = useCallback(() => { + dispatch(clearRequestedAttachmentDownload()); + onPreNavigate?.(); + if (isPN) { + trackPNAttachmentOpening(attachmentCategory); + NavigationService.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { + screen: PN_ROUTES.MAIN, + params: { + screen: PN_ROUTES.MESSAGE_ATTACHMENT, + params: { + attachmentId, + messageId + } + } + }); + } else { + NavigationService.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { + screen: MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT, + params: { + messageId, + serviceId, + attachmentId + } + }); + } + }, [ + attachmentCategory, + attachmentId, + dispatch, + isPN, + messageId, + onPreNavigate, + serviceId + ]); + const checkPathAndNavigate = useCallback( + async (downloadPath: string) => { + if (await RNFS.exists(downloadPath)) { + doNavigate(); } else { - await taskDownloadFileIntoAndroidPublicFolder(attachment, path)(); + dispatch(clearRequestedAttachmentDownload()); } - } - }, [downloadPot, openPreview, attachment.category]); - - useEffect(() => { - const wasLoading = isLoading; - const isStillLoading = pot.isLoading(downloadPot); - - if (wasLoading && !isStillLoading) { - void openAttachment(); - } - setIsLoading(isStillLoading); - }, [downloadPot, isLoading, setIsLoading, openAttachment]); - - const downloadAttachmentIfNeeded = async () => { - if (pot.isLoading(downloadPot)) { + }, + [dispatch, doNavigate] + ); + const onModuleAttachmentPress = useCallback(async () => { + if (isFetching) { return; } - // Do not download the attachment for generic third party message - if (!downloadAttachmentBeforePreview) { + if (!isPN) { trackThirdPartyMessageAttachmentShowPreview(); - openPreview(attachment); - return; } - await pipe( - downloadPot, - pot.toOption, - TE.fromOption(() => undefined), - TE.chain(download => - TE.tryCatch( - () => RNFS.exists(download.path), - () => undefined - ) - ), - TE.filterOrElse(identity, () => undefined), - TE.mapLeft(() => { - dispatch( - downloadAttachment.request({ - ...attachment, - skipMixpanelTrackingOnFailure: false - }) - ); - }), - TE.chainW(() => - TE.tryCatch( - () => { - // We must dispatch this action in order to cancel any - // other download that may be running (since we support - // selecting other attachments while cancelling the - // previous selected attachment's download) - dispatch(cancelPreviousAttachmentDownload()); - return openAttachment(); - }, - () => undefined - ) - ) - )(); - }; - - const onAttachmentSelect = () => { - void downloadAttachmentIfNeeded(); - }; + // Make sure to cancel whatever download may already be running + dispatch(cancelPreviousAttachmentDownload()); + + if (download && (await RNFS.exists(download.path))) { + doNavigate(); + } else { + dispatch( + downloadAttachment.request({ + attachment, + messageId, + skipMixpanelTrackingOnFailure: isPN + }) + ); + } + }, [attachment, dispatch, download, doNavigate, isFetching, isPN, messageId]); - return { - downloadPot, - onAttachmentSelect - }; + useEffect(() => { + const state = store.getState(); + if ( + download && + isRequestedAttachmentDownloadSelector(state, messageId, attachmentId) + ) { + void checkPathAndNavigate(download.path); + } else if ( + hasErrorOccourredOnRequestedDownloadSelector( + state, + messageId, + attachmentId + ) + ) { + dispatch(clearRequestedAttachmentDownload()); + if (isPN) { + trackPNAttachmentDownloadFailure(attachmentCategory); + } + IOToast.error(I18n.t("messageDetails.attachments.failing.details")); + } + }, [ + attachmentCategory, + attachmentId, + checkPathAndNavigate, + dispatch, + doNavigate, + download, + isPN, + messageId, + store + ]); + + const displayName = attachmentDisplayName(attachment); + return { displayName, isFetching, onModuleAttachmentPress }; }; diff --git a/ts/features/messages/hooks/useLegacyAttachmentDownload.tsx b/ts/features/messages/hooks/useLegacyAttachmentDownload.tsx new file mode 100644 index 00000000000..b6962dd6f6c --- /dev/null +++ b/ts/features/messages/hooks/useLegacyAttachmentDownload.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useState } from "react"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import ReactNativeBlobUtil from "react-native-blob-util"; +import RNFS from "react-native-fs"; +import { identity, pipe } from "fp-ts/lib/function"; +import * as E from "fp-ts/lib/Either"; +import * as TE from "fp-ts/lib/TaskEither"; +import i18n from "../../../i18n"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { ContentTypeValues } from "../types/contentType"; +import { isAndroid } from "../../../utils/platform"; +import { showToast } from "../../../utils/showToast"; +import { + cancelPreviousAttachmentDownload, + downloadAttachment +} from "../store/actions"; +import { downloadPotForMessageAttachmentSelector } from "../store/reducers/downloads"; +import { isTestEnv } from "../../../utils/environment"; +import { trackPNAttachmentDownloadFailure } from "../../pn/analytics"; +import { trackThirdPartyMessageAttachmentShowPreview } from "../analytics"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { UIMessageId } from "../types"; +import { + attachmentContentType, + attachmentDisplayName +} from "../store/reducers/transformers"; + +const taskCopyToMediaStore = (name: string, mimeType: string, path: string) => + TE.tryCatch( + () => + ReactNativeBlobUtil.MediaCollection.copyToMediaStore( + { + name, + parentFolder: "", + mimeType + }, + "Download", + path + ), + E.toError + ); + +const taskAddCompleteDownload = (name: string, mime: string, path: string) => + TE.tryCatch( + () => + ReactNativeBlobUtil.android.addCompleteDownload({ + mime, + title: name, + showNotification: true, + description: name, + path + }), + E.toError + ); + +const taskDownloadFileIntoAndroidPublicFolder = ( + name: string, + mimeType: string, + path: string +) => + pipe( + isAndroid, + TE.fromPredicate(identity, () => undefined), + TE.mapLeft(() => ReactNativeBlobUtil.ios.presentOptionsMenu(path)), + TE.chain(_ => + pipe( + taskCopyToMediaStore(name, mimeType, path), + TE.chain(downloadFilePath => + taskAddCompleteDownload(name, mimeType, downloadFilePath) + ), + TE.mapLeft(_ => + showToast(i18n.t("messageDetails.attachments.failing.details")) + ) + ) + ) + ); + +export const testableFunctions = isTestEnv + ? { + taskCopyToMediaStore, + taskAddCompleteDownload, + taskDownloadFileIntoAndroidPublicFolder + } + : undefined; + +// This hook has a different behaviour if the attachment is a PN +// one or a generic third-party attachment. +// When selecting a PN attachment, this hook takes care of downloading +// the attachment before going into the attachment preview component. +// If the attachment is from a third-party message (generic attachment) +// then the download is delegated to another part of the application +export const useLegacyAttachmentDownload = ( + attachment: ThirdPartyAttachment, + messageId: UIMessageId, + downloadAttachmentBeforePreview: boolean = false, + openPreview: (attachment: ThirdPartyAttachment) => void +) => { + const [isLoading, setIsLoading] = useState(false); + const dispatch = useIODispatch(); + + const downloadPot = useIOSelector(state => + downloadPotForMessageAttachmentSelector(state, messageId, attachment.id) + ); + + const openAttachment = useCallback(async () => { + const download = pot.toUndefined(downloadPot); + + if (pot.isError(downloadPot)) { + trackPNAttachmentDownloadFailure(attachment.category); + showToast(i18n.t("messageDetails.attachments.failing.details")); + } else if (download) { + const { path, attachment } = download; + + const contentType = attachmentContentType(attachment); + if (contentType === ContentTypeValues.applicationPdf) { + openPreview(attachment); + } else { + const name = attachmentDisplayName(attachment); + await taskDownloadFileIntoAndroidPublicFolder( + name, + contentType, + path + )(); + } + } + }, [downloadPot, openPreview, attachment.category]); + + useEffect(() => { + const wasLoading = isLoading; + const isStillLoading = pot.isLoading(downloadPot); + + if (wasLoading && !isStillLoading) { + void openAttachment(); + } + setIsLoading(isStillLoading); + }, [downloadPot, isLoading, setIsLoading, openAttachment]); + + const downloadAttachmentIfNeeded = async () => { + if (pot.isLoading(downloadPot)) { + return; + } + + // Do not download the attachment for generic third party message + if (!downloadAttachmentBeforePreview) { + trackThirdPartyMessageAttachmentShowPreview(); + openPreview(attachment); + return; + } + + await pipe( + downloadPot, + pot.toOption, + TE.fromOption(() => undefined), + TE.chain(download => + TE.tryCatch( + () => RNFS.exists(download.path), + () => undefined + ) + ), + TE.filterOrElse(identity, () => undefined), + TE.mapLeft(() => { + dispatch( + downloadAttachment.request({ + attachment, + messageId, + skipMixpanelTrackingOnFailure: true + }) + ); + }), + TE.chainW(() => + TE.tryCatch( + () => { + // We must dispatch this action in order to cancel any + // other download that may be running (since we support + // selecting other attachments while cancelling the + // previous selected attachment's download) + dispatch(cancelPreviousAttachmentDownload()); + return openAttachment(); + }, + () => undefined + ) + ) + )(); + }; + + const onAttachmentSelect = () => { + void downloadAttachmentIfNeeded(); + }; + + return { + downloadPot, + onAttachmentSelect + }; +}; diff --git a/ts/features/messages/hooks/useMessageCalendar.ts b/ts/features/messages/hooks/useMessageCalendar.ts new file mode 100644 index 00000000000..b7ffadc5e48 --- /dev/null +++ b/ts/features/messages/hooks/useMessageCalendar.ts @@ -0,0 +1,156 @@ +import { useCallback } from "react"; +import { Alert } from "react-native"; +import { Calendar } from "react-native-calendar-events"; +import { pipe } from "fp-ts/lib/function"; +import * as E from "fp-ts/lib/Either"; +import * as TE from "fp-ts/lib/TaskEither"; +import { IOToast } from "@pagopa/io-app-design-system"; +import I18n from "../../../i18n"; +import { + searchEventInCalendar, + convertLocalCalendarName, + saveEventToDeviceCalendarTask, + removeEventFromDeviceCalendarTask +} from "../../../utils/calendar"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { CalendarEvent } from "../../../store/reducers/entities/calendarEvents/calendarEventsByMessageId"; +import { preferredCalendarSelector } from "../../../store/reducers/persistedPreferences"; +import { + AddCalendarEventPayload, + addCalendarEvent, + removeCalendarEvent +} from "../../../store/actions/calendarEvents"; +import { preferredCalendarSaveSuccess } from "../../../store/actions/persistedPreferences"; +import { UIMessageId } from "../types"; + +export const useMessageCalendar = (messageId: UIMessageId) => { + const dispatch = useIODispatch(); + + const preferredCalendar = useIOSelector(preferredCalendarSelector); + + const handleAddEventToCalendar = useCallback( + (calendarEvent: AddCalendarEventPayload) => + dispatch(addCalendarEvent(calendarEvent)), + [dispatch] + ); + + const handleRemoveEventFromCalendar = useCallback( + (event: CalendarEvent) => + dispatch(removeCalendarEvent({ messageId: event.messageId })), + [dispatch] + ); + + const setPreferredCalendar = useCallback( + (preferredCalendar: Calendar) => + dispatch( + preferredCalendarSaveSuccess({ + preferredCalendar + }) + ), + [dispatch] + ); + + const onAddEventToCalendar = ( + calendar: Calendar, + dueDate: Date, + title: string + ) => { + void pipe( + saveEventToDeviceCalendarTask(calendar.id, dueDate, title), + TE.map(eventId => { + IOToast.success( + I18n.t("messages.cta.reminderAddSuccess", { + title, + calendarTitle: convertLocalCalendarName(calendar.title) + }) + ); + + // add event to the store + handleAddEventToCalendar({ messageId, eventId }); + }), + TE.mapLeft(() => IOToast.error(I18n.t("messages.cta.reminderAddFailure"))) + )(); + }; + + const handleConfirmAddEventToCalendar = ( + dueDate: Date, + eventId: string, + calendar: Calendar, + title: string + ) => { + Alert.alert( + I18n.t("messages.cta.reminderAlertTitle"), + I18n.t("messages.cta.reminderAlertDescription"), + [ + { + text: I18n.t("global.buttons.cancel"), + style: "cancel" + }, + { + text: I18n.t("messages.cta.reminderAlertKeep"), + style: "default", + onPress: () => { + // add event to the store + handleAddEventToCalendar({ + messageId, + eventId + }); + } + }, + { + text: I18n.t("messages.cta.reminderAlertAdd"), + style: "default", + onPress: () => onAddEventToCalendar(calendar, dueDate, title) + } + ], + { cancelable: false } + ); + }; + + const addEventToCalendar = ( + dueDate: Date, + eventTitle: string, + preferredCalendar: Calendar + ) => { + void pipe( + TE.tryCatch(() => searchEventInCalendar(dueDate, eventTitle), E.toError), + TE.chain(TE.fromOption(() => new Error("Event not found"))), + TE.map(eventId => + handleConfirmAddEventToCalendar( + dueDate, + eventId, + preferredCalendar, + eventTitle + ) + ), + TE.mapLeft(() => + onAddEventToCalendar(preferredCalendar, dueDate, eventTitle) + ) + )(); + }; + + const removeEventFromCalendar = ( + calendarEvent: CalendarEvent | undefined + ) => { + void pipe( + calendarEvent?.eventId, + TE.fromNullable(Error("calendarEvent not defined")), + TE.chain(removeEventFromDeviceCalendarTask), + TE.map(eventId => { + IOToast.success(I18n.t("messages.cta.reminderRemoveSuccess")); + + handleRemoveEventFromCalendar({ messageId, eventId }); + }), + TE.mapLeft(() => + IOToast.error(I18n.t("messages.cta.reminderRemoveFailure")) + ) + )(); + }; + + return { + preferredCalendar, + setPreferredCalendar, + addEventToCalendar, + removeEventFromCalendar + }; +}; diff --git a/ts/features/messages/hooks/useMessageOpening.tsx b/ts/features/messages/hooks/useMessageOpening.tsx index c02fab36504..15f06a4fc86 100644 --- a/ts/features/messages/hooks/useMessageOpening.tsx +++ b/ts/features/messages/hooks/useMessageOpening.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { View } from "react-native"; -import { useNavigation } from "@react-navigation/native"; import { constNull, pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { IOColors } from "@pagopa/io-app-design-system"; @@ -33,6 +32,7 @@ import { } from "../components/PreconditionBottomSheet/PreconditionContent"; import { PreconditionFooter } from "../components/PreconditionBottomSheet/PreconditionFooter"; import { MESSAGES_ROUTES } from "../navigation/routes"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; const renderPreconditionHeader = ( content: RemoteValue @@ -88,7 +88,7 @@ const renderPreconditionFooter = ( }; export const useMessageOpening = () => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const dispatch = useIODispatch(); const pnSupported = useIOSelector(isPnSupportedSelector); diff --git a/ts/features/messages/hooks/useMessageReminder.ts b/ts/features/messages/hooks/useMessageReminder.ts new file mode 100644 index 00000000000..b2a9cf58522 --- /dev/null +++ b/ts/features/messages/hooks/useMessageReminder.ts @@ -0,0 +1,117 @@ +import { useEffect, useState } from "react"; +import { Alert } from "react-native"; +import { Calendar } from "react-native-calendar-events"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as TE from "fp-ts/lib/TaskEither"; +import I18n from "../../../i18n"; +import { + isEventInCalendar, + requestCalendarPermission +} from "../../../utils/calendar"; +import { openAppSettings } from "../../../utils/appSettings"; +import { useIOSelector } from "../../../store/hooks"; +import { calendarEventByMessageIdSelector } from "../../../store/reducers/entities/calendarEvents/calendarEventsByMessageId"; +import { UIMessageId } from "../types"; +import { useMessageCalendar } from "./useMessageCalendar"; + +export const useMessageReminder = ( + messageId: UIMessageId, + selectCalendar: () => void +) => { + const [isEventInDeviceCalendar, setIsEventInDeviceCalendar] = + useState(false); + + const calendarEvent = useIOSelector(state => + calendarEventByMessageIdSelector(state, messageId) + ); + + const { addEventToCalendar, removeEventFromCalendar } = + useMessageCalendar(messageId); + + /** + * Hook that checks if the event is in the device calendar + */ + useEffect(() => { + void pipe( + calendarEvent, + O.fromNullable, + TE.fromOption(() => new Error("Event not found")), + TE.chain(event => isEventInCalendar(event.eventId)), + TE.map(setIsEventInDeviceCalendar), + TE.mapLeft(() => setIsEventInDeviceCalendar(false)) + )(); + }, [calendarEvent]); + + const upsertReminder = async ( + dueDate: Date, + subject: string, + preferredCalendar: Calendar | undefined + ) => { + const permissionGranted = await requestCalendarPermission(); + + // Authorized is false (denied, restricted or undetermined) + // If the user denied permission previously (not in this session) + // prompt an alert to inform that his calendar permissions could have been turned off + if (!permissionGranted) { + Alert.alert( + I18n.t("global.genericAlert"), + I18n.t("messages.cta.calendarPermDenied.title"), + [ + { + text: I18n.t("messages.cta.calendarPermDenied.cancel"), + style: "cancel" + }, + { + text: I18n.t("messages.cta.calendarPermDenied.ok"), + style: "default", + onPress: openAppSettings + } + ], + { cancelable: true } + ); + + return; + } + + // If the event is in the calendar prompt an alert and ask for confirmation + if (isEventInDeviceCalendar) { + Alert.alert( + I18n.t("messages.cta.reminderRemoveRequest.title"), + undefined, + [ + { + text: I18n.t("messages.cta.reminderRemoveRequest.cancel"), + style: "cancel" + }, + { + text: I18n.t("messages.cta.reminderRemoveRequest.ok"), + style: "destructive", + onPress: () => removeEventFromCalendar(calendarEvent) + } + ], + { cancelable: false } + ); + + return; + } + + if (preferredCalendar) { + const title = I18n.t("messages.cta.reminderTitle", { + title: subject + }); + + addEventToCalendar(dueDate, title, preferredCalendar); + + return; + } + + // Navigate to the screen to let the user select a calendar + selectCalendar(); + }; + + return { + isEventInDeviceCalendar, + upsertReminder + }; +}; diff --git a/ts/features/messages/navigation/MessagesHomeTabNavigator.tsx b/ts/features/messages/navigation/MessagesHomeTabNavigator.tsx index 91efe43dd76..eedf1bd910f 100644 --- a/ts/features/messages/navigation/MessagesHomeTabNavigator.tsx +++ b/ts/features/messages/navigation/MessagesHomeTabNavigator.tsx @@ -24,16 +24,17 @@ const MessagesHomeTabNavigator = () => ( ( height: 34 } }} - lazy={true} > { return ( - + + + + - + - + - + {isPnEnabled && ( + + )} + - {isPnEnabled && ( + - )} + ); }; diff --git a/ts/features/messages/navigation/params.ts b/ts/features/messages/navigation/params.ts index ae6f1f95751..209bc4eff36 100644 --- a/ts/features/messages/navigation/params.ts +++ b/ts/features/messages/navigation/params.ts @@ -1,17 +1,19 @@ import { NavigatorScreenParams } from "@react-navigation/native"; import EUCOVIDCERT_ROUTES from "../../euCovidCert/navigation/routes"; import PN_ROUTES from "../../pn/navigation/routes"; -import { MessageRouterScreenNavigationParams } from "../screens/MessageRouterScreen"; -import { MessageDetailScreenNavigationParams } from "../screens/MessageDetailScreen"; +import { MessageRouterScreenRouteParams } from "../screens/MessageRouterScreen"; +import { MessageDetailsScreenRouteParams } from "../screens/MessageDetailsScreen"; import { EUCovidCertParamsList } from "../../euCovidCert/navigation/params"; import { PnParamsList } from "../../pn/navigation/params"; -import { MessageAttachmentNavigationParams } from "../screens/MessageAttachment"; +import { MessageAttachmentScreenRouteParams } from "../screens/MessageAttachmentScreen"; +import { MessageCalendarScreenRouteParams } from "../screens/MessageCalendarScreen"; import { MESSAGES_ROUTES } from "./routes"; export type MessagesParamsList = { - [MESSAGES_ROUTES.MESSAGE_ROUTER]: MessageRouterScreenNavigationParams; - [MESSAGES_ROUTES.MESSAGE_DETAIL]: MessageDetailScreenNavigationParams; - [MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT]: MessageAttachmentNavigationParams; + [MESSAGES_ROUTES.MESSAGE_ROUTER]: MessageRouterScreenRouteParams; + [MESSAGES_ROUTES.MESSAGE_DETAIL]: MessageDetailsScreenRouteParams; + [MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT]: MessageAttachmentScreenRouteParams; + [MESSAGES_ROUTES.MESSAGE_DETAIL_CALENDAR]: MessageCalendarScreenRouteParams; [EUCOVIDCERT_ROUTES.MAIN]: NavigatorScreenParams; [PN_ROUTES.MAIN]: NavigatorScreenParams; }; diff --git a/ts/features/messages/navigation/routes.ts b/ts/features/messages/navigation/routes.ts index e832df06bdf..8023c7d64c1 100644 --- a/ts/features/messages/navigation/routes.ts +++ b/ts/features/messages/navigation/routes.ts @@ -3,5 +3,6 @@ export const MESSAGES_ROUTES = { MESSAGES_HOME: "MESSAGES_HOME", MESSAGE_ROUTER: "MESSAGE_ROUTER", MESSAGE_DETAIL: "MESSAGE_DETAIL", - MESSAGE_DETAIL_ATTACHMENT: "MESSAGE_DETAIL_ATTACHMENT" + MESSAGE_DETAIL_ATTACHMENT: "MESSAGE_DETAIL_ATTACHMENT", + MESSAGE_DETAIL_CALENDAR: "MESSAGE_DETAIL_CALENDAR" } as const; diff --git a/ts/features/messages/saga/__test__/handleDownloadAttachment.test.ts b/ts/features/messages/saga/__test__/handleDownloadAttachment.test.ts index f521707505e..69db2e31079 100644 --- a/ts/features/messages/saga/__test__/handleDownloadAttachment.test.ts +++ b/ts/features/messages/saga/__test__/handleDownloadAttachment.test.ts @@ -12,6 +12,7 @@ import { lollipopKeyTagSelector, lollipopPublicKeySelector } from "../../../lollipop/store/reducers/lollipop"; +import { messageId_1 } from "../../__mocks__/messages"; const savePath = "/tmp/attachment.pdf"; const serviceId = "service0000001" as ServiceId; @@ -42,7 +43,8 @@ describe("downloadAttachment given an attachment", () => { downloadAttachmentWorker, "token" as SessionToken, downloadAttachment.request({ - ...attachment, + attachment, + messageId: messageId_1, skipMixpanelTrackingOnFailure: false }) ) @@ -62,6 +64,7 @@ describe("downloadAttachment given an attachment", () => { .put( downloadAttachment.success({ attachment, + messageId: messageId_1, path: savePath }) ) @@ -74,7 +77,8 @@ describe("downloadAttachment given an attachment", () => { downloadAttachmentWorker, "token" as SessionToken, downloadAttachment.request({ - ...attachment, + attachment, + messageId: messageId_1, skipMixpanelTrackingOnFailure: false }) ) @@ -93,6 +97,7 @@ describe("downloadAttachment given an attachment", () => { .put( downloadAttachment.failure({ attachment, + messageId: messageId_1, error: new Error( I18n.t("messageDetails.attachments.downloadFailed") ) @@ -107,7 +112,8 @@ describe("downloadAttachment given an attachment", () => { downloadAttachmentWorker, "token" as SessionToken, downloadAttachment.request({ - ...attachment, + attachment, + messageId: messageId_1, skipMixpanelTrackingOnFailure: false }) ) @@ -126,6 +132,7 @@ describe("downloadAttachment given an attachment", () => { .put( downloadAttachment.failure({ attachment, + messageId: messageId_1, error: new Error(I18n.t("messageDetails.attachments.badFormat")) }) ) diff --git a/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts b/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts index 239fd3cc570..8458d31c086 100644 --- a/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts +++ b/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts @@ -12,7 +12,7 @@ import { upsertMessageStatusAttributes } from "../../store/actions"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { serviceByIdSelector } from "../../../../store/reducers/entities/services/servicesById"; +import { serviceByIdPotSelector } from "../../../services/store/reducers/servicesById"; import { loadServiceDetail } from "../../../../store/actions/services"; import { messageDetailsByIdSelector } from "../../store/reducers/detailsById"; import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; @@ -101,7 +101,7 @@ describe("getService", () => { const serviceId = "s1" as ServiceId; testSaga(testable!.getService, serviceId) .next() - .select(serviceByIdSelector, serviceId) + .select(serviceByIdPotSelector, serviceId) .next(pot.none) .put(loadServiceDetail.request(serviceId)) .next() @@ -111,7 +111,7 @@ describe("getService", () => { const serviceId = "s1" as ServiceId; testSaga(testable!.getService, serviceId) .next() - .select(serviceByIdSelector, serviceId) + .select(serviceByIdPotSelector, serviceId) .next(pot.noneError) .put(loadServiceDetail.request(serviceId)) .next() @@ -121,7 +121,7 @@ describe("getService", () => { const serviceId = "s1" as ServiceId; testSaga(testable!.getService, serviceId) .next() - .select(serviceByIdSelector, serviceId) + .select(serviceByIdPotSelector, serviceId) .next(pot.someError({}, new Error())) .put(loadServiceDetail.request(serviceId)) .next() @@ -131,7 +131,7 @@ describe("getService", () => { const serviceId = "s1" as ServiceId; testSaga(testable!.getService, serviceId) .next() - .select(serviceByIdSelector, serviceId) + .select(serviceByIdPotSelector, serviceId) .next(pot.some({})) .isDone(); }); diff --git a/ts/features/messages/saga/__test__/handleRequestInit.test.ts b/ts/features/messages/saga/__test__/handleRequestInit.test.ts index 65a6987e8e2..39a0763db74 100644 --- a/ts/features/messages/saga/__test__/handleRequestInit.test.ts +++ b/ts/features/messages/saga/__test__/handleRequestInit.test.ts @@ -5,13 +5,14 @@ import { handleRequestInit, testableHandleRequestInitFactory } from "../handleRequestInit"; -import { UIAttachment } from "../../types"; import { lollipopKeyTagSelector, lollipopPublicKeySelector } from "../../../lollipop/store/reducers/lollipop"; import { generateKeyInfo } from "../../../lollipop/saga"; import { lollipopRequestInit } from "../../../lollipop/utils/fetch"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { messageId_1 } from "../../__mocks__/messages"; const handleRequestInitFactory = testableHandleRequestInitFactory!; @@ -29,13 +30,15 @@ describe("handleDownloadAttachment", () => { it("handleRequestInit should follow the proper flow and return the enhanced lollipop headers", () => { const data = fetchParametersCommonInputData(); + const attachmentFullUrl = `undefined/api/v1/third-party-messages/${data.messageId}/attachments/${data.attachmentFullUrl}`; testSaga( handleRequestInit, { - resourceUrl: { - href: data.attachmentFullUrl - } - } as UIAttachment, + id: data.attachmentId, + name: data.attachmentName, + url: data.attachmentFullUrl + } as ThirdPartyAttachment, + data.messageId, data.bearerToken, data.nonce ) @@ -50,26 +53,28 @@ describe("handleDownloadAttachment", () => { lollipopRequestInit, { nonce: data.nonce }, data.keyInfo, - data.attachmentFullUrl, + attachmentFullUrl, { headers: data.headers, method: "GET" } ) .next({ headers: data.enhancedHeaders }) .returns({ method: "GET", - attachmentFullUrl: data.attachmentFullUrl, + attachmentFullUrl, headers: data.enhancedHeaders }); }); it("handleRequestInit should follow the proper flow and return standard headers when lollipopRequestInit fails", () => { const data = fetchParametersCommonInputData(); + const attachmentFullUrl = `undefined/api/v1/third-party-messages/${data.messageId}/attachments/${data.attachmentFullUrl}`; testSaga( handleRequestInit, { - resourceUrl: { - href: data.attachmentFullUrl - } - } as UIAttachment, + id: data.attachmentId, + name: data.attachmentName, + url: data.attachmentFullUrl + } as ThirdPartyAttachment, + data.messageId, data.bearerToken, data.nonce ) @@ -84,13 +89,13 @@ describe("handleDownloadAttachment", () => { lollipopRequestInit, { nonce: data.nonce }, data.keyInfo, - data.attachmentFullUrl, + attachmentFullUrl, { headers: data.headers, method: "GET" } ) .next({ headers: undefined }) .returns({ method: "GET", - attachmentFullUrl: data.attachmentFullUrl, + attachmentFullUrl, headers: data.headers }); }); @@ -118,6 +123,9 @@ const fetchParametersCommonInputData = () => { publicKey, publicKeyThumbprint: "thumbprint" }, + messageId: messageId_1, + attachmentId: "1", + attachmentName: "1.pdf", attachmentFullUrl: "https://my.attachment/full/url", headers, enhancedHeaders: { diff --git a/ts/features/messages/saga/handleDownloadAttachment.ts b/ts/features/messages/saga/handleDownloadAttachment.ts index aaddf73c202..e485c734910 100644 --- a/ts/features/messages/saga/handleDownloadAttachment.ts +++ b/ts/features/messages/saga/handleDownloadAttachment.ts @@ -23,7 +23,7 @@ import { cancelPreviousAttachmentDownload, downloadAttachment } from "../store/actions"; -import { UIAttachment, UIMessageId } from "../types"; +import { UIMessageId } from "../types"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { getServiceByMessageId } from "../store/reducers/paginatedById"; import { @@ -32,6 +32,7 @@ import { trackThirdPartyMessageAttachmentUnavailable } from "../analytics"; import { getHeaderByKey } from "../utils/strings"; +import { attachmentDisplayName } from "../store/reducers/transformers"; import { handleRequestInit } from "./handleRequestInit"; export const AttachmentsDirectoryPath = @@ -41,14 +42,8 @@ export const AttachmentsDirectoryPath = * Builds the save path for the given attachment * @param attachment */ -const savePath = (attachment: UIAttachment) => - AttachmentsDirectoryPath + - "/" + - attachment.messageId + - "/" + - attachment.id + - "/" + - attachment.displayName; +const savePath = (messageId: UIMessageId, attachmentId: string, name: string) => + `${AttachmentsDirectoryPath}/${messageId}/${attachmentId}/${name}`; const getDelayMilliseconds = (headers: Record) => pipe( @@ -80,20 +75,23 @@ export function* downloadAttachmentWorker( bearerToken: SessionToken, action: ActionType ): SagaIterator { - const { skipMixpanelTrackingOnFailure, ...attachment } = action.payload; - const messageId = attachment.messageId; + const { attachment, messageId, skipMixpanelTrackingOnFailure } = + action.payload; const serviceId = yield* select(getServiceByMessageId, messageId); + const name = attachmentDisplayName(attachment); + while (true) { try { const config = yield* call(ReactNativeBlobUtil.config, { - path: savePath(attachment), + path: savePath(messageId, attachment.id, name), timeout: fetchTimeout }); const { method, attachmentFullUrl, headers } = yield* call( handleRequestInit, attachment, + messageId, bearerToken, uuid() ); @@ -109,7 +107,7 @@ export function* downloadAttachmentWorker( if (status === 200) { const path = result.path(); - yield* put(downloadAttachment.success({ attachment, path })); + yield* put(downloadAttachment.success({ attachment, messageId, path })); } else if (status === 503) { const waitingMs = getDelayMilliseconds(rest.headers); if (waitingMs >= 0) { @@ -121,7 +119,7 @@ export function* downloadAttachmentWorker( trackFailureEvent( skipMixpanelTrackingOnFailure, status, - attachment.messageId, + messageId, serviceId ); // In this case we produce a taking error that can be @@ -131,18 +129,16 @@ export function* downloadAttachmentWorker( ? "messageDetails.attachments.badFormat" : "messageDetails.attachments.downloadFailed"; const error = new Error(I18n.t(errorKey)); - yield* put(downloadAttachment.failure({ attachment, error })); + yield* put( + downloadAttachment.failure({ attachment, messageId, error }) + ); } } catch (error) { - trackFailureEvent( - skipMixpanelTrackingOnFailure, - 0, - attachment.messageId, - serviceId - ); + trackFailureEvent(skipMixpanelTrackingOnFailure, 0, messageId, serviceId); yield* put( downloadAttachment.failure({ attachment, + messageId, error: getError(error) }) ); @@ -150,7 +146,7 @@ export function* downloadAttachmentWorker( // In this way, the download pot's status // in the reducer will be properly updated. if (yield* cancelled()) { - yield* put(downloadAttachment.cancel(attachment)); + yield* put(downloadAttachment.cancel({ attachment, messageId })); } } break; diff --git a/ts/features/messages/saga/handleLoadMessageData.ts b/ts/features/messages/saga/handleLoadMessageData.ts index 2a0edb099eb..8b89b4e3de5 100644 --- a/ts/features/messages/saga/handleLoadMessageData.ts +++ b/ts/features/messages/saga/handleLoadMessageData.ts @@ -18,7 +18,7 @@ import { } from "../store/actions"; import { getPaginatedMessageById } from "../store/reducers/paginatedById"; import { UIMessage, UIMessageDetails, UIMessageId } from "../types"; -import { serviceByIdSelector } from "../../../store/reducers/entities/services/servicesById"; +import { serviceByIdPotSelector } from "../../services/store/reducers/servicesById"; import { loadServiceDetail } from "../../../store/actions/services"; import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; import { thirdPartyFromIdSelector } from "../store/reducers/thirdPartyById"; @@ -173,7 +173,7 @@ function* getPaginatedMessage(messageId: UIMessageId) { } function* getService(serviceId: ServiceId) { - const servicePot = yield* select(serviceByIdSelector, serviceId); + const servicePot = yield* select(serviceByIdPotSelector, serviceId); if (!pot.isSome(servicePot) || pot.isError(servicePot)) { yield* put(loadServiceDetail.request(serviceId)); } diff --git a/ts/features/pn/store/sagas/watchPaymentUpdateRequests.ts b/ts/features/messages/saga/handlePaymentUpdateRequests.ts similarity index 86% rename from ts/features/pn/store/sagas/watchPaymentUpdateRequests.ts rename to ts/features/messages/saga/handlePaymentUpdateRequests.ts index 9ee737b83f7..430823922f6 100644 --- a/ts/features/pn/store/sagas/watchPaymentUpdateRequests.ts +++ b/ts/features/messages/saga/handlePaymentUpdateRequests.ts @@ -3,17 +3,17 @@ import { Channel, buffers, channel } from "redux-saga"; import { call, flush, fork, put, take } from "typed-redux-saga/macro"; import { ActionType, isActionOf } from "typesafe-actions"; import { RptIdFromString } from "@pagopa/io-pagopa-commons/lib/pagopa"; -import { BackendClient } from "../../../../api/backend"; +import { BackendClient } from "../../../api/backend"; +import { commonPaymentVerificationProcedure } from "../../../sagas/wallet/pagopaApis"; +import { Detail_v2Enum } from "../../../../definitions/backend/PaymentProblemJson"; import { - updatePaymentForMessage, - cancelQueuedPaymentUpdates -} from "../actions"; -import { commonPaymentVerificationProcedure } from "../../../../sagas/wallet/pagopaApis"; -import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; + cancelQueuedPaymentUpdates, + updatePaymentForMessage +} from "../store/actions"; const generatePaymentUpdateWorkerCount = () => 5; -export function* watchPaymentUpdateRequests( +export function* handlePaymentUpdateRequests( getVerificaRpt: ReturnType["getVerificaRpt"] ) { // create a channel to queue incoming requests @@ -28,7 +28,7 @@ export function* watchPaymentUpdateRequests( // eslint-disable-next-line functional/no-let for (let i = 0; i < paymentUpdateWorkerCount; i++) { yield* fork( - handlePaymentUpdateRequests, + paymentUpdateRequestWorker, paymentUpdateChannel, getVerificaRpt ); @@ -56,7 +56,7 @@ export function* watchPaymentUpdateRequests( } } -function* handlePaymentUpdateRequests( +function* paymentUpdateRequestWorker( paymentStatusChannel: Channel< ActionType >, diff --git a/ts/features/messages/saga/handleRequestInit.ts b/ts/features/messages/saga/handleRequestInit.ts index b44062de94a..88c04054db5 100644 --- a/ts/features/messages/saga/handleRequestInit.ts +++ b/ts/features/messages/saga/handleRequestInit.ts @@ -1,5 +1,4 @@ import { call, select } from "typed-redux-saga/macro"; -import { UIAttachment } from "../types"; import { lollipopKeyTagSelector, lollipopPublicKeySelector @@ -8,11 +7,15 @@ import { generateKeyInfo } from "../../lollipop/saga"; import { LollipopConfig } from "../../lollipop"; import { lollipopRequestInit } from "../../lollipop/utils/fetch"; import { isTestEnv } from "../../../utils/environment"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { attachmentDownloadUrl } from "../store/reducers/transformers"; +import { UIMessageId } from "../types"; type HeaderType = Record; export function* handleRequestInit( - attachment: UIAttachment, + attachment: ThirdPartyAttachment, + messageId: UIMessageId, bearerToken: string, nonce: string ) { @@ -24,7 +27,7 @@ export function* handleRequestInit( const publicKey = yield* select(lollipopPublicKeySelector); const keyInfo = yield* call(generateKeyInfo, keyTag, publicKey); - const attachmentFullUrl = attachment.resourceUrl.href; + const attachmentFullUrl = attachmentDownloadUrl(messageId, attachment); const requestInit = { headers: { diff --git a/ts/features/messages/saga/index.ts b/ts/features/messages/saga/index.ts index ac52ba4bafa..a4cacd70da3 100644 --- a/ts/features/messages/saga/index.ts +++ b/ts/features/messages/saga/index.ts @@ -40,6 +40,7 @@ import { handleUpsertMessageStatusAttribues } from "./handleUpsertMessageStatusA import { handleMigrateToPagination } from "./handleMigrateToPagination"; import { handleMessagePrecondition } from "./handleMessagePrecondition"; import { handleThirdPartyMessage } from "./handleThirdPartyMessage"; +import { handlePaymentUpdateRequests } from "./handlePaymentUpdateRequests"; /** * Handle messages requests @@ -113,6 +114,9 @@ export function* watchMessagesSaga( bearerToken ); + // handle the request for updating a message's payment + yield* fork(handlePaymentUpdateRequests, backendClient.getVerificaRpt); + // handle the request for removing a downloaded attachment yield* takeEvery(removeCachedAttachment, handleClearAttachment); diff --git a/ts/features/messages/screens/LegacyMessageAttachment.tsx b/ts/features/messages/screens/LegacyMessageAttachment.tsx index d60ee13fd11..2a409ad1563 100644 --- a/ts/features/messages/screens/LegacyMessageAttachment.tsx +++ b/ts/features/messages/screens/LegacyMessageAttachment.tsx @@ -8,7 +8,7 @@ import { MessagesParamsList } from "../navigation/params"; import { showToast } from "../../../utils/showToast"; import { getServiceByMessageId } from "../store/reducers/paginatedById"; import { useIOSelector } from "../../../store/hooks"; -import { thirdPartyMessageUIAttachment } from "../store/reducers/thirdPartyById"; +import { thirdPartyMessageAttachment } from "../store/reducers/thirdPartyById"; import { trackThirdPartyMessageAttachmentCorruptedFile, trackThirdPartyMessageAttachmentPreviewSuccess, @@ -32,7 +32,7 @@ export const LegacyMessageDetailAttachment = ( ); const maybeThirdPartyMessageUIAttachment = useIOSelector(state => - thirdPartyMessageUIAttachment(state)(messageId)(attachmentId) + thirdPartyMessageAttachment(state)(messageId)(attachmentId) ); useEffect(() => { diff --git a/ts/features/messages/screens/MessageDetailScreen.tsx b/ts/features/messages/screens/LegacyMessageDetailScreen.tsx similarity index 68% rename from ts/features/messages/screens/MessageDetailScreen.tsx rename to ts/features/messages/screens/LegacyMessageDetailScreen.tsx index fbe0d6e1c45..fb8664993e7 100644 --- a/ts/features/messages/screens/MessageDetailScreen.tsx +++ b/ts/features/messages/screens/LegacyMessageDetailScreen.tsx @@ -1,48 +1,45 @@ +import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { pipe } from "fp-ts/lib/function"; +import { Route, useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import React from "react"; import { - View, ActivityIndicator, SafeAreaView, - StyleSheet + StyleSheet, + View } from "react-native"; -import { connect } from "react-redux"; -import { VSpacer } from "@pagopa/io-app-design-system"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { Body } from "../../../components/core/typography/Body"; import { IOStyles } from "../../../components/core/variables/IOStyles"; import WorkunitGenericFailure from "../../../components/error/WorkunitGenericFailure"; -import MessageDetailComponent from "../components/MessageDetail"; -import ErrorState from "../components/MessageDetail/ErrorState"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { isNoticePaidSelector } from "../../../store/reducers/entities/payments"; -import { - loadMessageDetails, - resetGetMessageDataAction -} from "../store/actions"; import { navigateBack, navigateToServiceDetailsScreen } from "../../../store/actions/navigation"; import { loadServiceDetail } from "../../../store/actions/services"; -import { Dispatch, ReduxProps } from "../../../store/actions/types"; -import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; -import { getPaginatedMessageById } from "../store/reducers/paginatedById"; -import { UIMessageId } from "../types"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { isNoticePaidSelector } from "../../../store/reducers/entities/payments"; import { - serviceByIdSelector, + serviceByIdPotSelector, serviceMetadataByIdSelector -} from "../../../store/reducers/entities/services/servicesById"; +} from "../../services/store/reducers/servicesById"; import { toUIService } from "../../../store/reducers/entities/services/transformers"; -import { GlobalState } from "../../../store/reducers/types"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; -import { MessagesParamsList } from "../navigation/params"; +import MessageDetailComponent from "../components/MessageDetail"; +import ErrorState from "../components/MessageDetail/ErrorState"; +import { + loadMessageDetails, + resetGetMessageDataAction +} from "../store/actions"; +import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; +import { getPaginatedMessageById } from "../store/reducers/paginatedById"; +import { UIMessageId } from "../types"; const styles = StyleSheet.create({ notFullStateContainer: { @@ -57,16 +54,6 @@ export type MessageDetailScreenNavigationParams = { serviceId: ServiceId; }; -type OwnProps = IOStackNavigationRouteProps< - MessagesParamsList, - "MESSAGE_DETAIL" ->; - -type Props = OwnProps & - ReturnType & - ReturnType & - ReduxProps; - const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "messageDetails.contextualHelpTitle", body: "messageDetails.contextualHelpContent" @@ -80,24 +67,48 @@ const renderLoadingState = () => ( ); -const MessageDetailScreen = ({ - goBack, - hasPaidBadge, - loadMessageDetails, - maybeServiceMetadata, - messageId, - serviceId, - message, - messageDetails, - refreshService, - service -}: Props) => { +const LegacyMessageDetailScreen = () => { + const { messageId, serviceId } = + useRoute>() + .params; + const dispatch = useIODispatch(); + + const refreshService = (serviceId: string) => + dispatch(loadServiceDetail.request(serviceId)); + const requestLoadMessageDetails = (id: UIMessageId) => + dispatch(loadMessageDetails.request({ id })); + const goBack = () => { + dispatch(resetGetMessageDataAction()); + return navigateBack(); + }; + + const message = pot.toUndefined( + useIOSelector(state => getPaginatedMessageById(state, messageId)) + ); + const messageDetails = useIOSelector(state => + messageDetailsByIdSelector(state, messageId) + ); + const service = pipe( + pot.toOption( + useIOSelector(state => serviceByIdPotSelector(state, serviceId)) + ), + O.map(toUIService), + O.toUndefined + ); + // Map the potential message to the potential service + const maybeServiceMetadata = useIOSelector(state => + serviceMetadataByIdSelector(state, serviceId) + ); + const hasPaidBadge: boolean = useIOSelector(state => + message ? isNoticePaidSelector(state, message.category) : false + ); + useOnFirstRender(() => { if ( pot.isError(messageDetails) || (pot.isNone(messageDetails) && !pot.isLoading(messageDetails)) ) { - loadMessageDetails(messageId); + requestLoadMessageDetails(messageId); } }); @@ -112,7 +123,7 @@ const MessageDetailScreen = ({ const onRetry = () => { // we try to reload both the message content and the service refreshService(serviceId); - loadMessageDetails(messageId); + requestLoadMessageDetails(messageId); }; const renderErrorState = () => ( @@ -159,45 +170,4 @@ const MessageDetailScreen = ({ ); }; -const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { - const messageId = ownProps.route.params.messageId; - const serviceId = ownProps.route.params.serviceId; - const message = pot.toUndefined(getPaginatedMessageById(state, messageId)); - const messageDetails = messageDetailsByIdSelector(state, messageId); - const service = pipe( - pot.toOption(serviceByIdSelector(state, serviceId)), - O.map(toUIService), - O.toUndefined - ); - // Map the potential message to the potential service - const maybeServiceMetadata = serviceMetadataByIdSelector(serviceId)(state); - const hasPaidBadge: boolean = message - ? isNoticePaidSelector(state, message.category) - : false; - - return { - messageId, - serviceId, - hasPaidBadge, - message, - messageDetails, - maybeServiceMetadata, - service - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - refreshService: (serviceId: string) => - dispatch(loadServiceDetail.request(serviceId)), - loadMessageDetails: (id: UIMessageId) => - dispatch(loadMessageDetails.request({ id })), - goBack: () => { - dispatch(resetGetMessageDataAction()); - return navigateBack(); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(MessageDetailScreen); +export default LegacyMessageDetailScreen; diff --git a/ts/features/messages/screens/MessageAttachmentScreen.tsx b/ts/features/messages/screens/MessageAttachmentScreen.tsx new file mode 100644 index 00000000000..05e8f99e487 --- /dev/null +++ b/ts/features/messages/screens/MessageAttachmentScreen.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { UIMessageId } from "../types"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { MessagesParamsList } from "../navigation/params"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { MessageAttachment } from "../components/MessageAttachment/MessageAttachment"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; + +export type MessageAttachmentScreenRouteParams = { + messageId: UIMessageId; + attachmentId: string; + serviceId?: ServiceId; +}; + +type MessageAttachmentScreenProps = IOStackNavigationRouteProps< + MessagesParamsList, + "MESSAGE_DETAIL_ATTACHMENT" +>; + +export const MessageAttachmentScreen = ( + props: MessageAttachmentScreenProps +) => { + const { attachmentId, messageId, serviceId } = props.route.params; + + useHeaderSecondLevel({ + title: "", + supportRequest: true + }); + + return ( + + ); +}; diff --git a/ts/features/messages/screens/MessageCalendarScreen.tsx b/ts/features/messages/screens/MessageCalendarScreen.tsx new file mode 100644 index 00000000000..666df47dd16 --- /dev/null +++ b/ts/features/messages/screens/MessageCalendarScreen.tsx @@ -0,0 +1,148 @@ +import React, { ComponentProps, useCallback, useEffect, useState } from "react"; +import { ScrollView, View } from "react-native"; +import { Calendar } from "react-native-calendar-events"; +import { RouteProp, useRoute } from "@react-navigation/native"; +import { + FooterWithButtons, + H2, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as TE from "fp-ts/lib/TaskEither"; +import I18n from "../../../i18n"; +import { UIMessageId } from "../types"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import { MessagesParamsList } from "../navigation/params"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; +import { BaseHeader } from "../../../components/screens/BaseHeader"; +import { CalendarList } from "../../../components/CalendarList"; +import { useIOSelector } from "../../../store/hooks"; +import { findDeviceCalendarsTask } from "../../../utils/calendar"; +import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; +import { useMessageCalendar } from "../hooks/useMessageCalendar"; + +export type MessageCalendarScreenRouteParams = { + messageId: UIMessageId; +}; + +type MessageCalendarRouteProps = RouteProp< + MessagesParamsList, + "MESSAGE_DETAIL_CALENDAR" +>; + +export const MessageCalendarScreen = () => { + const [calendarsByAccount, setCalendarsByAccount] = useState< + pot.Pot, Error> + >(pot.noneLoading); + + const { params } = useRoute(); + const { messageId } = params; + + const navigation = useIONavigation(); + + const messageDetails = useIOSelector(state => + messageDetailsByIdSelector(state, messageId) + ); + + const { addEventToCalendar, setPreferredCalendar } = + useMessageCalendar(messageId); + + const handleCalendarSelected = useCallback( + (calendar: Calendar) => { + pipe( + messageDetails, + pot.toOption, + O.map(({ subject, dueDate }) => { + if (!dueDate) { + return; + } + addEventToCalendar(dueDate, subject, calendar); + setPreferredCalendar(calendar); + navigation.goBack(); + }) + ); + }, + [messageDetails, addEventToCalendar, setPreferredCalendar, navigation] + ); + + const fetchCalendars = useCallback(async () => { + setCalendarsByAccount(pot.noneLoading); + + void pipe( + findDeviceCalendarsTask, + TE.map(calendars => { + setCalendarsByAccount(pot.some(calendars)); + }), + TE.mapLeft(error => { + setCalendarsByAccount(pot.toError(pot.none, error)); + }) + )(); + }, []); + + useEffect(() => { + void fetchCalendars(); + }, [fetchCalendars]); + + const closeIconButton: ComponentProps["customRightIcon"] = + { + iconName: "closeLarge", + accessibilityLabel: I18n.t("accessibility.buttons.torch.turnOff"), + onPress: () => navigation.goBack() + }; + + if (pot.isError(calendarsByAccount)) { + return ( + <> + {/* FIXME: replace with new header */} + + + + ); + } + + return ( + <> + + + {/* FIXME: replace with new header */} + +

{I18n.t("messages.cta.reminderCalendarSelect")}

+
+ + + >(() => []) + )} + isLoading={pot.isLoading(calendarsByAccount)} + onCalendarSelected={handleCalendarSelected} + /> + +
+ navigation.goBack() + } + }} + /> + + ); +}; diff --git a/ts/features/messages/screens/MessageDetailsScreen.tsx b/ts/features/messages/screens/MessageDetailsScreen.tsx new file mode 100644 index 00000000000..1cab220c234 --- /dev/null +++ b/ts/features/messages/screens/MessageDetailsScreen.tsx @@ -0,0 +1,221 @@ +import React, { useCallback, useMemo } from "react"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { ContentWrapper, Tag, VSpacer } from "@pagopa/io-app-design-system"; +import { useFocusEffect } from "@react-navigation/native"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { UIMessageId } from "../types"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { MessagesParamsList } from "../navigation/params"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../../navigation/params/AppParamsList"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { useIODispatch, useIOSelector, useIOStore } from "../../../store/hooks"; +import { + cancelPreviousAttachmentDownload, + cancelQueuedPaymentUpdates, + updatePaymentForMessage +} from "../store/actions"; +import { getPaginatedMessageById } from "../store/reducers/paginatedById"; +import { + hasAttachmentsSelector, + messageMarkdownSelector, + messageTitleSelector +} from "../store/reducers/thirdPartyById"; +import { MessageDetailsAttachments } from "../components/MessageDetail/MessageDetailsAttachments"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; +import { MessageDetailsHeader } from "../components/MessageDetail/MessageDetailsHeader"; +import I18n from "../../../i18n"; +import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; +import { MessageDetailsTagBox } from "../components/MessageDetail/MessageDetailsTagBox"; +import { MessageMarkdown } from "../components/MessageDetail/MessageMarkdown"; +import { cleanMarkdownFromCTAs, getMessageCTA } from "../utils/messages"; +import { MessageDetailsReminder } from "../components/MessageDetail/MessageDetailsReminder"; +import { MessageDetailsFooter } from "../components/MessageDetail/MessageDetailsFooter"; +import { MessageDetailsPayment } from "../components/MessageDetail/MessageDetailsPayment"; +import { cancelPaymentStatusTracking } from "../../pn/store/actions"; +import { userSelectedPaymentRptIdSelector } from "../store/reducers/payments"; +import { MessageDetailsStickyFooter } from "../components/MessageDetail/MessageDetailsStickyFooter"; +import { MessageDetailsScrollViewAdditionalSpace } from "../components/MessageDetail/MessageDetailsScrollViewAdditionalSpace"; +import { serviceMetadataByIdSelector } from "../../services/store/reducers/servicesById"; +import { isPNOptInMessage } from "../../pn/utils"; +import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; +import { + trackPNOptInMessageCTADisplaySuccess, + trackPNOptInMessageOpened +} from "../../pn/analytics"; + +const styles = StyleSheet.create({ + scrollContentContainer: { + flexGrow: 1 + }, + container: { + flexGrow: 1 + } +}); + +export type MessageDetailsScreenRouteParams = { + messageId: UIMessageId; + serviceId: ServiceId; +}; + +type MessageDetailsScreenProps = IOStackNavigationRouteProps< + MessagesParamsList, + "MESSAGE_DETAIL" +>; + +export const MessageDetailsScreen = (props: MessageDetailsScreenProps) => { + const { messageId, serviceId } = props.route.params; + + const navigation = useIONavigation(); + + const dispatch = useIODispatch(); + + const message = pipe( + useIOSelector(state => getPaginatedMessageById(state, messageId)), + pot.toOption, + O.toUndefined + ); + + const messageDetails = pipe( + useIOSelector(state => messageDetailsByIdSelector(state, messageId)), + pot.toOption, + O.toUndefined + ); + + const hasAttachments = useIOSelector(state => + hasAttachmentsSelector(state, messageId) + ); + + const subject = + useIOSelector(state => messageTitleSelector(state, messageId)) ?? ""; + + const goBack = useCallback(() => { + dispatch(cancelPreviousAttachmentDownload()); + dispatch(cancelQueuedPaymentUpdates()); + dispatch(cancelPaymentStatusTracking()); + navigation.goBack(); + }, [dispatch, navigation]); + + const messageMarkdown = + useIOSelector(state => messageMarkdownSelector(state, messageId)) ?? ""; + const markdownWithNoCTA = useMemo( + () => cleanMarkdownFromCTAs(messageMarkdown), + [messageMarkdown] + ); + const serviceMetadata = useIOSelector(state => + serviceMetadataByIdSelector(state, serviceId) + ); + const maybeCTAs = useMemo( + () => + pipe( + getMessageCTA(messageMarkdown, serviceMetadata, serviceId), + O.toUndefined + ), + [messageMarkdown, serviceId, serviceMetadata] + ); + + // Use the store since `isPNOptInMessage` is not a selector but an utility + // that uses a backend status configuration that is normally updated every + // minute. We do not want to cause a re-rendering or recompute the value + const store = useIOStore(); + const state = store.getState(); + const pnOptInMessageInfo = isPNOptInMessage(maybeCTAs, serviceId, state); + + useHeaderSecondLevel({ + title: "", + goBack, + supportRequest: true + }); + + useOnFirstRender( + () => { + trackPNOptInMessageOpened(); + trackPNOptInMessageCTADisplaySuccess(); + }, + () => pnOptInMessageInfo.isPNOptInMessage + ); + + useFocusEffect( + useCallback(() => { + const globalState = store.getState(); + const paymentToCheckRptId = userSelectedPaymentRptIdSelector( + globalState, + messageDetails + ); + if (paymentToCheckRptId) { + dispatch( + updatePaymentForMessage.request({ + messageId, + paymentId: paymentToCheckRptId + }) + ); + } + }, [dispatch, messageId, messageDetails, store]) + ); + + if (message === undefined || messageDetails === undefined) { + return ( + + ); + } + + return ( + <> + + + + + {hasAttachments && ( + + + + )} + + + + {markdownWithNoCTA} + + + + + + + + + + + + ); +}; diff --git a/ts/features/messages/screens/MessageRouterScreen.tsx b/ts/features/messages/screens/MessageRouterScreen.tsx index 13a36486d2c..aecf36b5835 100644 --- a/ts/features/messages/screens/MessageRouterScreen.tsx +++ b/ts/features/messages/screens/MessageRouterScreen.tsx @@ -1,9 +1,12 @@ -import { StackActions, useNavigation } from "@react-navigation/native"; +import { StackActions } from "@react-navigation/native"; import React, { useCallback, useEffect, useRef } from "react"; import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import { LoadingErrorComponent } from "../../../components/LoadingErrorComponent"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../../navigation/params/AppParamsList"; import { MessagesParamsList } from "../navigation/params"; import ROUTES from "../../../navigation/routes"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; @@ -25,7 +28,7 @@ import EUCOVIDCERT_ROUTES from "../../euCovidCert/navigation/routes"; import PN_ROUTES from "../../pn/navigation/routes"; import { MESSAGES_ROUTES } from "../navigation/routes"; -export type MessageRouterScreenNavigationParams = { +export type MessageRouterScreenRouteParams = { messageId: UIMessageId; fromNotification: boolean; }; @@ -41,7 +44,7 @@ export const MessageRouterScreen = ( const messageId = props.route.params.messageId; const fromPushNotification = props.route.params.fromNotification; const dispatch = useIODispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const isFirstRendering = useRef(true); const showSpinner = useIOSelector(showSpinnerFromMessageGetStatusSelector); const thirdPartyMessageDetailsError = useIOSelector( diff --git a/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx b/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx index 5531553a12f..20dcd97ccc2 100644 --- a/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx +++ b/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx @@ -1,25 +1,26 @@ import { createStore } from "redux"; -import { UIAttachment, UIAttachmentId, UIMessageId } from "../../types"; +import { UIMessageId } from "../../types"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; import { MESSAGES_ROUTES } from "../../navigation/routes"; import { appReducer } from "../../../../store/reducers"; import { applicationChangeState } from "../../../../store/actions/application"; -import { MessageAttachment } from "../MessageAttachment"; +import { MessageAttachmentScreen } from "../MessageAttachmentScreen"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { downloadAttachment } from "../../store/actions"; import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; describe("MessageAttachment", () => { it("Should match the snapshot when there is an error", () => { const messageId = "01HMZWRG7549N76017YR8YBSG2" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const serviceId = "01HMZXFS84T1Q1BN6GXRYT63VJ" as ServiceId; const screen = renderScreen(messageId, attachmentId, serviceId, "failure"); expect(screen.toJSON()).toMatchSnapshot(); }); it("Should match the snapshot when everything went fine", () => { const messageId = "01HMZWRG7549N76017YR8YBSG2" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const serviceId = "01HMZXFS84T1Q1BN6GXRYT63VJ" as ServiceId; const screen = renderScreen(messageId, attachmentId, serviceId, "success"); expect(screen.toJSON()).toMatchSnapshot(); @@ -28,7 +29,7 @@ describe("MessageAttachment", () => { const renderScreen = ( messageId: UIMessageId, - attachmentId: UIAttachmentId, + attachmentId: string, serviceId: ServiceId, configuration: "failure" | "success" ) => { @@ -40,7 +41,8 @@ const renderScreen = ( const withDownloadState = appReducer( designSystemState, downloadAttachment.success({ - attachment: { id: attachmentId, messageId } as UIAttachment, + attachment: { id: attachmentId } as ThirdPartyAttachment, + messageId, path: "file:///fileName.pdf" }) ); @@ -50,7 +52,7 @@ const renderScreen = ( ); return renderScreenWithNavigationStoreContext( - MessageAttachment, + MessageAttachmentScreen, MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT, { messageId, attachmentId, isPN: false, serviceId }, store diff --git a/ts/features/messages/screens/__tests__/MessageDetailsScreen.test.tsx b/ts/features/messages/screens/__tests__/MessageDetailsScreen.test.tsx new file mode 100644 index 00000000000..c9c8f6b3191 --- /dev/null +++ b/ts/features/messages/screens/__tests__/MessageDetailsScreen.test.tsx @@ -0,0 +1,172 @@ +import { Action, Store, createStore } from "redux"; +import { MESSAGES_ROUTES } from "../../navigation/routes"; +import { GlobalState } from "../../../../store/reducers/types"; +import { appReducer } from "../../../../store/reducers"; +import { MessageDetailsScreen } from "../MessageDetailsScreen"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { reproduceSequence } from "../../../../utils/tests"; +import { + loadMessageById, + loadMessageDetails, + loadThirdPartyMessage +} from "../../../messages/store/actions"; +import { + toUIMessage, + toUIMessageDetails +} from "../../../messages/store/reducers/transformers"; +import { + messageWithExpairedPayment, + messageWithValidPayment, + message_1 +} from "../../../messages/__mocks__/message"; +import { loadServiceDetail } from "../../../../store/actions/services"; +import { service_1 } from "../../../messages/__mocks__/messages"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; +import { ATTACHMENT_CATEGORY } from "../../types/attachmentCategory"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { UIMessageId } from "../../types"; + +export const thirdPartyMessage: ThirdPartyMessageWithContent = { + ...message_1, + created_at: new Date("2020-01-01T00:00:00.000Z"), + third_party_message: { + attachments: [ + { + id: "1", + name: "A First Attachment", + content_type: "application/pdf", + category: ATTACHMENT_CATEGORY.DOCUMENT, + url: "/resource/attachment1.pdf" + }, + { + id: "2", + name: "A Second Attachment", + content_type: "application/pdf", + category: ATTACHMENT_CATEGORY.DOCUMENT, + url: "/resource/attachment2.pdf" + } + ] as Array + } +}; + +describe("MessageDetailsScreen", () => { + it("should display the attachment tag if there are attachments", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageById.success(toUIMessage(message_1)), + loadServiceDetail.success(service_1), + loadMessageDetails.success( + toUIMessageDetails(messageWithExpairedPayment) + ), + loadThirdPartyMessage.success({ + id: message_1.id as UIMessageId, + content: thirdPartyMessage + }) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const store: Store = createStore(appReducer, state as any); + + const { component } = renderComponent(store); + expect(component.queryByTestId("attachment-tag")).not.toBeNull(); + }); + + it("should NOT display the attachment tag if there are no attachments", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageById.success(toUIMessage(message_1)), + loadServiceDetail.success(service_1), + loadMessageDetails.success( + toUIMessageDetails(messageWithExpairedPayment) + ), + loadThirdPartyMessage.success({ + id: message_1.id as UIMessageId, + content: { + ...thirdPartyMessage, + third_party_message: { + attachments: [] + } + } + }) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const store: Store = createStore(appReducer, state as any); + + const { component } = renderComponent(store); + expect(component.queryByTestId("attachment-tag")).toBeNull(); + }); + + it("should display the alert banner if the payment is expiring", () => { + const next7Days = new Date(new Date().setDate(new Date().getDate() + 7)); + + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageById.success(toUIMessage(message_1)), + loadServiceDetail.success(service_1), + loadMessageDetails.success( + toUIMessageDetails({ + ...messageWithValidPayment, + content: { + ...messageWithValidPayment.content, + due_date: next7Days + } + }) + ) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const store: Store = createStore(appReducer, state as any); + + const { component } = renderComponent(store); + expect(component.queryByTestId("due-date-alert")).toBeNull(); + }); + + it("should NOT display the alert banner if the payment is NOT expiring", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageById.success(toUIMessage(message_1)), + loadServiceDetail.success(service_1), + loadMessageDetails.success(toUIMessageDetails(messageWithValidPayment)) + ]; + + const state: Partial = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const store: Store = createStore(appReducer, state as any); + + const { component } = renderComponent(store); + expect(component.queryByTestId("due-date-alert")).toBeNull(); + }); +}); + +const renderComponent = (store: Store) => { + const { id, sender_service_id } = message_1; + + return { + component: renderScreenWithNavigationStoreContext( + MessageDetailsScreen, + MESSAGES_ROUTES.MESSAGE_DETAIL, + { + messageId: id, + serviceId: sender_service_id + }, + store + ) + }; +}; diff --git a/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap b/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap index 18daafbbdfa..1084341ee88 100644 --- a/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap +++ b/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MessageAttachment Should match the snapshot when there is an error 1`] = ` +exports[`MessageAttachment Should match the snapshot when everything went fine 1`] = ` - - - - - - - - - - - - - - PDF Preview - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - + + + + + + + + + + + + + + + + - + Loading document... + + + + + + + + + + + + + - + - + + + Save or share + + + + + + + + + + + + + + + + - - + + + + + + + + + + + - + -
- - + > + + + + + + +
+
- - There is a temporary problem, please try again. - - - - If the problem is not resolved, report it with the '?' icon at the top right, thank you! - - - +
+
-
-
+ + + `; -exports[`MessageAttachment Should match the snapshot when everything went fine 1`] = ` +exports[`MessageAttachment Should match the snapshot when there is an error 1`] = ` - - - - - - - - - - - - - - PDF Preview - - - - - - - - - - - - - - - - - - - - + - @@ -1228,74 +1177,56 @@ exports[`MessageAttachment Should match the snapshot when everything went fine 1 "flex": 1, } } - testID="overlayComponent" > - - - + "current": null, + }, + ] + } + > + - - - + - - - + + - + + - + - + } + propList={ + Array [ + "fill", + ] + } + /> + + + - - + style={ + Array [ + Object { + "textAlign": "center", + }, + Object { + "fontSize": 24, + "lineHeight": 34, + }, + Object { + "color": "#17324D", + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="SemiBold" + > + There is a temporary problem, please try again. + + - Loading document... + If the problem is not resolved, report it with the '?' icon at the top right, thank you! - - - + + + + + + + + + + + + + + - - Save or share - + + + + @@ -1630,8 +1799,22 @@ exports[`MessageAttachment Should match the snapshot when everything went fine 1 - - + + + diff --git a/ts/features/messages/screens/__tests__/__snapshots__/MessageRouterScreen.test.tsx.snap b/ts/features/messages/screens/__tests__/__snapshots__/MessageRouterScreen.test.tsx.snap index 067edc12415..eec0d148972 100644 --- a/ts/features/messages/screens/__tests__/__snapshots__/MessageRouterScreen.test.tsx.snap +++ b/ts/features/messages/screens/__tests__/__snapshots__/MessageRouterScreen.test.tsx.snap @@ -20,622 +20,665 @@ exports[`MessageRouterScreen should match snapshot before starting to retrieve m } > - - - + + /> + + - + - MESSAGE_ROUTER - + + MESSAGE_ROUTER + + + - - - - - + - - + - - + - - - + - - + > + + + + - - - - + > + + + > + + - - + + - - - - - + + + + Loading message details in progress... + + - Loading message details in progress... - - + /> + - - + + - - + + @@ -661,622 +704,665 @@ exports[`MessageRouterScreen should match snapshot if message data retrieval was } > - - - + + /> + + - + - MESSAGE_ROUTER - + + MESSAGE_ROUTER + + + - - - - - + - - - - - + + + - - - + - - + > + + + + - - - - + > + + + > + + - - + + - - - - - + + + + Loading message details in progress... + + - Loading message details in progress... - - + /> + - - + + - - + + @@ -1302,630 +1388,643 @@ exports[`MessageRouterScreen should match snapshot on message data retrieval fai } > - - - + + /> + + - + - MESSAGE_ROUTER - + + MESSAGE_ROUTER + + + - - - - - + - - + - - + - - - + - - + > + + + + - - - - + > + + + > + + - - + + - - - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + resetScrollToCoords={ + Object { + "x": 0, + "y": 0, + } + } + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={true} + style={ + Object { + "backgroundColor": "transparent", + "flex": 1, + } + } + testID="LoadingErrorComponentError" + update={[Function]} + viewIsInsideTabBar={false} + > + - + - There is a temporary problem, please try again. - + /> - + + There is a temporary problem, please try again. + + + + + - - - - - + + - - Cancel - - - - + + Cancel + + + + - - Retry - + + Retry + + - - + + - - + + @@ -2238,622 +2366,665 @@ exports[`MessageRouterScreen should match snapshot on message data retrieval suc } > - - - + + /> + + - + - MESSAGE_ROUTER - + + MESSAGE_ROUTER + + + - - - - - + - - + - - + - - - + - - + > + + + + - - - - + > + + + > + + - - + + - - - - - + + + + Loading message details in progress... + + - Loading message details in progress... - - + /> + - - + + - - + + @@ -2879,622 +3050,665 @@ exports[`MessageRouterScreen should match snapshot while retrieving message data } > - - - + + /> + + - + - MESSAGE_ROUTER - + + MESSAGE_ROUTER + + + - - - - - + - - + - - + - - - + - - + > + + + + - - - - + > + + + > + + - - + + - - - - - + + + + Loading message details in progress... + + - Loading message details in progress... - - + /> + - - + + - - + + diff --git a/ts/features/messages/store/actions/index.ts b/ts/features/messages/store/actions/index.ts index 2b6a2c418b1..a7f52ecb680 100644 --- a/ts/features/messages/store/actions/index.ts +++ b/ts/features/messages/store/actions/index.ts @@ -7,18 +7,18 @@ import { import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { - UIAttachment, UIMessage, UIMessageDetails, UIMessageId, - WithSkipMixpanelTrackingOnFailure, WithUIMessageId } from "../../types"; import { MessageGetStatusFailurePhaseType } from "../reducers/messageGetStatus"; import { MessageCategory } from "../../../../../definitions/backend/MessageCategory"; import { ThirdPartyMessagePrecondition } from "../../../../../definitions/backend/ThirdPartyMessagePrecondition"; -import { Download, DownloadError } from "../reducers/downloads"; import { MessagesStatus } from "../reducers/messagesStatus"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; export type ThirdPartyMessageActions = ActionType; @@ -219,6 +219,29 @@ export const resetMigrationStatus = createAction( "MESSAGES_MIGRATE_TO_PAGINATED_DONE" ); +export type DownloadAttachmentRequest = { + attachment: ThirdPartyAttachment; + messageId: UIMessageId; + skipMixpanelTrackingOnFailure: boolean; +}; + +export type DownloadAttachmentSuccess = { + attachment: ThirdPartyAttachment; + messageId: UIMessageId; + path: string; +}; + +export type DownloadAttachmentError = { + attachment: ThirdPartyAttachment; + error: Error; + messageId: UIMessageId; +}; + +export type DownloadAttachmentCancel = { + attachment: ThirdPartyAttachment; + messageId: UIMessageId; +}; + /** * The user requests an attachment download. */ @@ -228,22 +251,67 @@ export const downloadAttachment = createAsyncAction( "DOWNLOAD_ATTACHMENT_FAILURE", "DOWNLOAD_ATTACHMENT_CANCEL" )< - WithSkipMixpanelTrackingOnFailure, - Download, - DownloadError, - UIAttachment + DownloadAttachmentRequest, + DownloadAttachmentSuccess, + DownloadAttachmentError, + DownloadAttachmentCancel >(); export const cancelPreviousAttachmentDownload = createAction( "CANCEL_PREVIOUS_ATTACHMENT_DOWNLOAD" ); +export const clearRequestedAttachmentDownload = createAction( + "CLEAR_REQUESTED_ATTACHMNET_DOWNLOAD" +); + /** * This action removes any cached data in order to perform another download. */ export const removeCachedAttachment = createStandardAction( "REMOVE_CACHED_ATTACHMENT" -)(); +)(); + +export type UpdatePaymentForMessageRequest = { + messageId: UIMessageId; + paymentId: string; +}; + +export type UpdatePaymentForMessageSuccess = { + messageId: UIMessageId; + paymentId: string; + paymentData: PaymentRequestsGetResponse; +}; + +export type UpdatePaymentForMessageFailure = { + messageId: UIMessageId; + paymentId: string; + details: Detail_v2Enum; +}; + +export type UpdatePaymentForMessageCancel = + ReadonlyArray; + +export const updatePaymentForMessage = createAsyncAction( + "UPDATE_PAYMENT_FOR_MESSAGE_REQUEST", + "UPDATE_PAYMENT_FOR_MESSAGE_SUCCESS", + "UPDATE_PAYMENT_FOR_MESSAGE_FAILURE", + "UPDATE_PAYMENT_FOR_MESSAGE_CANCEL" +)< + UpdatePaymentForMessageRequest, + UpdatePaymentForMessageSuccess, + UpdatePaymentForMessageFailure, + UpdatePaymentForMessageCancel +>(); + +export const cancelQueuedPaymentUpdates = createAction( + "CANCEL_QUEUED_PAYMENT_UPDATES" +); + +export const addUserSelectedPaymentRptId = createAction( + "MESSAGES_ADD_USER_SELECTED_PAYMENT_RPTID", + resolve => (paymentId: string) => resolve({ paymentId }) +); export type MessagesActions = ActionType< | typeof reloadAllMessages @@ -258,10 +326,14 @@ export type MessagesActions = ActionType< | typeof loadThirdPartyMessage | typeof downloadAttachment | typeof cancelPreviousAttachmentDownload + | typeof clearRequestedAttachmentDownload | typeof removeCachedAttachment | typeof getMessagePrecondition | typeof clearMessagePrecondition | typeof getMessageDataAction | typeof cancelGetMessageDataAction | typeof resetGetMessageDataAction + | typeof updatePaymentForMessage + | typeof cancelQueuedPaymentUpdates + | typeof addUserSelectedPaymentRptId >; diff --git a/ts/features/messages/store/actions/navigation.ts b/ts/features/messages/store/actions/navigation.ts index aaf9def0108..b13bcd8c2ad 100644 --- a/ts/features/messages/store/actions/navigation.ts +++ b/ts/features/messages/store/actions/navigation.ts @@ -1,13 +1,13 @@ import { CommonActions } from "@react-navigation/native"; import { MESSAGES_ROUTES } from "../../navigation/routes"; -import { MessageDetailScreenNavigationParams } from "../../screens/MessageDetailScreen"; -import { MessageRouterScreenNavigationParams } from "../../screens/MessageRouterScreen"; +import { MessageRouterScreenRouteParams } from "../../screens/MessageRouterScreen"; +import { MessageDetailsScreenRouteParams } from "../../screens/MessageDetailsScreen"; /** * Open the Message Detail screen supporting the new UIMessage type. */ export const navigateToMessageDetailScreenAction = ( - params: MessageDetailScreenNavigationParams + params: MessageDetailsScreenRouteParams ) => CommonActions.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { screen: MESSAGES_ROUTES.MESSAGE_DETAIL, @@ -18,7 +18,7 @@ export const navigateToMessageDetailScreenAction = ( * Open the Message Detail Router supporting the new UIMessage type. */ export const navigateToMessageRouterAction = ( - params: MessageRouterScreenNavigationParams + params: MessageRouterScreenRouteParams ) => CommonActions.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { screen: MESSAGES_ROUTES.MESSAGE_ROUTER, diff --git a/ts/features/messages/store/reducers/__tests__/detailsById.test.ts b/ts/features/messages/store/reducers/__tests__/detailsById.test.ts index 4128a4afd77..79a0750d2f7 100644 --- a/ts/features/messages/store/reducers/__tests__/detailsById.test.ts +++ b/ts/features/messages/store/reducers/__tests__/detailsById.test.ts @@ -6,13 +6,17 @@ import { } from "../../../__mocks__/message"; import { loadMessageDetails } from "../../actions"; -import reducer, { - detailedMessageHasThirdPartyDataSelector, - messageDetailsByIdSelector -} from "../detailsById"; -import { UIMessageDetails, UIMessageId } from "../../../types"; +import { PaymentData, UIMessageDetails, UIMessageId } from "../../../types"; import { applicationChangeState } from "../../../../../store/actions/application"; import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { + detailedMessageHasThirdPartyDataSelector, + detailsByIdReducer, + messageDetailsByIdSelector, + messageDetailsExpiringInfoSelector, + messagePaymentDataSelector +} from "../detailsById"; const id = paymentValidInvalidAfterDueDate.id as UIMessageId; @@ -20,13 +24,15 @@ describe("detailsById reducer", () => { describe(`when a ${getType(loadMessageDetails.request)} is sent`, () => { const actionRequest = loadMessageDetails.request({ id }); it(`should add an entry for ${id} with 'noneLoading'`, () => { - expect(reducer(undefined, actionRequest)[id]).toEqual(pot.noneLoading); + expect(detailsByIdReducer(undefined, actionRequest)[id]).toEqual( + pot.noneLoading + ); }); describe(`and an entry for ${id} already exists`, () => { const initialState = { [id]: pot.some(successLoadMessageDetails) }; it(`should update the entry to loading state preserving the data`, () => { - const entry = reducer(initialState, actionRequest)[id]; + const entry = detailsByIdReducer(initialState, actionRequest)[id]; expect(pot.isLoading(entry)).toBe(true); expect(pot.isSome(entry)).toBe(true); expect(pot.toUndefined(entry)).toBeDefined(); @@ -37,7 +43,7 @@ describe("detailsById reducer", () => { describe(`when a ${getType(loadMessageDetails.success)} is sent`, () => { const actionRequest = loadMessageDetails.success(successLoadMessageDetails); it(`should add an entry for ${id}`, () => { - const entry = reducer(undefined, actionRequest)[id]; + const entry = detailsByIdReducer(undefined, actionRequest)[id]; expect(pot.isSome(entry)).toBe(true); expect(pot.toUndefined(entry)).toEqual(successLoadMessageDetails); }); @@ -50,7 +56,7 @@ describe("detailsById reducer", () => { error }); it(`should add an entry for ${id} with 'noneError'`, () => { - expect(reducer(undefined, actionRequest)[id]).toEqual( + expect(detailsByIdReducer(undefined, actionRequest)[id]).toEqual( pot.noneError(error.message) ); }); @@ -58,7 +64,7 @@ describe("detailsById reducer", () => { describe(`and an entry for ${id} already exists`, () => { const initialState = { [id]: pot.some(successLoadMessageDetails) }; it(`should update the entry to error state preserving the data`, () => { - const entry = reducer(initialState, actionRequest)[id]; + const entry = detailsByIdReducer(initialState, actionRequest)[id]; expect(pot.isError(entry)).toBe(true); expect(pot.isSome(entry)).toBe(true); expect(pot.toUndefined(entry)).toBeDefined(); @@ -121,3 +127,165 @@ describe("detailedMessageHasThirdPartyDataSelector", () => { expect(hasThirdPartyData).toBe(true); }); }); + +describe("messageDetailsExpiringInfoSelector", () => { + it("should return `does_not_expire` when `paymentData` is not defined", () => { + const messageId = "m1" as UIMessageId; + const action = loadMessageDetails.success({ + id: messageId + } as UIMessageDetails); + const state = appReducer(undefined, action); + + const expiringInfo = messageDetailsExpiringInfoSelector( + state, + messageId, + new Date("01/05/2023").getTime() + ); + expect(expiringInfo).toBe("does_not_expire"); + }); + + it("should return `does_not_expire` when `paymentData` is defined and `dueDate` is not", () => { + const messageId = "m1" as UIMessageId; + const action = loadMessageDetails.success({ + id: messageId, + paymentData: { + amount: 99, + noticeNumber: "123", + payee: { + fiscalCode: "123" + } + } + } as UIMessageDetails); + const state = appReducer(undefined, action); + + const expiringInfo = messageDetailsExpiringInfoSelector( + state, + messageId, + new Date("01/05/2023").getTime() + ); + expect(expiringInfo).toBe("does_not_expire"); + }); + + it("should return `expired` when there is a `paymentData` is defined and `dueDate` has passed", () => { + const messageId = "m1" as UIMessageId; + const action = loadMessageDetails.success({ + id: messageId, + dueDate: new Date("01/01/2023"), + paymentData: { + amount: 99, + noticeNumber: "123", + payee: { + fiscalCode: "123" + } + } + } as UIMessageDetails); + const state = appReducer(undefined, action); + + const expiringInfo = messageDetailsExpiringInfoSelector( + state, + messageId, + new Date("01/05/2023").getTime() + ); + expect(expiringInfo).toBe("expired"); + }); + + it("should return `expiring` when there is a `paymentData` is defined and `dueDate` has not passed", () => { + const messageId = "m1" as UIMessageId; + const action = loadMessageDetails.success({ + id: messageId, + dueDate: new Date("01/05/2023"), + paymentData: { + amount: 99, + noticeNumber: "123", + payee: { + fiscalCode: "123" + } + } + } as UIMessageDetails); + const state = appReducer(undefined, action); + + const expiringInfo = messageDetailsExpiringInfoSelector( + state, + messageId, + new Date("01/01/2023").getTime() + ); + expect(expiringInfo).toBe("expiring"); + }); +}); + +describe("messagePaymentData selector", () => { + it("should return undefined when the state is empty", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const messageId = "01HR9ZVVKPQDGQ97TT83AN1W8C" as UIMessageId; + const paymentData = messagePaymentDataSelector(appState, messageId); + expect(paymentData).toBeUndefined(); + }); + it("should return undefined when there is no message match", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRA095RSRSY7Z2DJQ0H8N3TR": pot.some({ + paymentData: {} + } as UIMessageDetails) + } + } + } + } as GlobalState; + const paymentData = messagePaymentDataSelector( + finalState, + "01HR9ZVVKPQDGQ97TT83AN1W8C" as UIMessageId + ); + expect(paymentData).toBeUndefined(); + }); + it("should return undefined when the message has no payment data", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HR9ZVVKPQDGQ97TT83AN1W8C": pot.some({} as UIMessageDetails) + } + } + } + } as GlobalState; + const paymentData = messagePaymentDataSelector( + finalState, + "01HR9ZVVKPQDGQ97TT83AN1W8C" as UIMessageId + ); + expect(paymentData).toBeUndefined(); + }); + it("should match returned payment data", () => { + const paymentData = {} as PaymentData; + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HR9ZVVKPQDGQ97TT83AN1W8C": pot.some({ + paymentData + } as UIMessageDetails) + } + } + } + } as GlobalState; + const returnedPaymentData = messagePaymentDataSelector( + finalState, + "01HR9ZVVKPQDGQ97TT83AN1W8C" as UIMessageId + ); + expect(returnedPaymentData).toBe(paymentData); + }); +}); diff --git a/ts/features/messages/store/reducers/__tests__/downloads.test.ts b/ts/features/messages/store/reducers/__tests__/downloads.test.ts index 49bee044cdd..14cb4db2f93 100644 --- a/ts/features/messages/store/reducers/__tests__/downloads.test.ts +++ b/ts/features/messages/store/reducers/__tests__/downloads.test.ts @@ -1,34 +1,42 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { mockPdfAttachment } from "../../../__mocks__/attachment"; -import { downloadAttachment, removeCachedAttachment } from "../../actions"; import { - Download, - DownloadError, + DownloadAttachmentCancel, + DownloadAttachmentError, + DownloadAttachmentRequest, + DownloadAttachmentSuccess, + clearRequestedAttachmentDownload, + downloadAttachment, + removeCachedAttachment +} from "../../actions"; +import { Downloads, INITIAL_STATE, + downloadPotForMessageAttachmentSelector, downloadedMessageAttachmentSelector, - downloadsReducer + downloadsReducer, + hasErrorOccourredOnRequestedDownloadSelector, + isDownloadingMessageAttachmentSelector, + isRequestedAttachmentDownloadSelector } from "../downloads"; -import { - UIAttachment, - UIAttachmentId, - UIMessageId, - WithSkipMixpanelTrackingOnFailure -} from "../../../types"; +import { UIMessageId } from "../../../types"; import { GlobalState } from "../../../../../store/reducers/types"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { ThirdPartyAttachment } from "../../../../../../definitions/backend/ThirdPartyAttachment"; const path = "/path/attachment.pdf"; describe("downloadedMessageAttachmentSelector", () => { it("Should return undefined for an unmatching messageId", () => { - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const successDownload = { attachment: { - messageId: "01HMXFQ803Q8JGQECKQF0EX6KX" as UIMessageId, id: attachmentId - } as UIAttachment, + } as ThirdPartyAttachment, + messageId: "01HMXFQ803Q8JGQECKQF0EX6KX" as UIMessageId, path: "randomPath" - } as Download; + } as DownloadAttachmentSuccess; const downloadSuccessAction = downloadAttachment.success(successDownload); const downloadsState = downloadsReducer( INITIAL_STATE, @@ -54,11 +62,11 @@ describe("downloadedMessageAttachmentSelector", () => { const unrelatedAttachmentId = "2"; const successDownload = { attachment: { - messageId, id: unrelatedAttachmentId - } as UIAttachment, + } as ThirdPartyAttachment, + messageId, path: "randomPath" - } as Download; + } as DownloadAttachmentSuccess; const downloadSuccessAction = downloadAttachment.success(successDownload); const downloadsState = downloadsReducer( INITIAL_STATE, @@ -71,7 +79,7 @@ describe("downloadedMessageAttachmentSelector", () => { } } } as GlobalState; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const downloadedAttachment = downloadedMessageAttachmentSelector( globalState, messageId, @@ -81,12 +89,12 @@ describe("downloadedMessageAttachmentSelector", () => { }); it("Should return undefined for an attachment that is loading", () => { const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const uiAttachmentRequest = { messageId, - id: attachmentId, + attachment: { id: attachmentId } as ThirdPartyAttachment, skipMixpanelTrackingOnFailure: true - } as WithSkipMixpanelTrackingOnFailure; + } as DownloadAttachmentRequest; const downloadRequestAction = downloadAttachment.request(uiAttachmentRequest); const downloadsState = downloadsReducer( @@ -109,14 +117,14 @@ describe("downloadedMessageAttachmentSelector", () => { }); it("Should return undefined for an attachment that got an error", () => { const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const failedDownload = { attachment: { - messageId, id: attachmentId - } as UIAttachment, + } as ThirdPartyAttachment, + messageId, error: new Error("An error") - } as DownloadError; + } as DownloadAttachmentError; const downloadFailureAction = downloadAttachment.failure(failedDownload); const downloadsState = downloadsReducer( INITIAL_STATE, @@ -138,11 +146,11 @@ describe("downloadedMessageAttachmentSelector", () => { }); it("Should return undefined for an attachment that was cancelled before finishing the download", () => { const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const uiAttachmentCancelled = { messageId, - id: attachmentId - } as UIAttachment; + attachment: { id: attachmentId } as ThirdPartyAttachment + } as DownloadAttachmentCancel; const downloadCancelAction = downloadAttachment.cancel( uiAttachmentCancelled ); @@ -166,14 +174,14 @@ describe("downloadedMessageAttachmentSelector", () => { }); it("Should return undefined for an attachment that was removed by a removeCachedAttachment action", () => { const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const successDownload = { attachment: { - messageId, id: attachmentId - } as UIAttachment, + } as ThirdPartyAttachment, + messageId, path: "randomPath" - } as Download; + } as DownloadAttachmentSuccess; const removedCachedAttachmentAction = removeCachedAttachment(successDownload); const downloadsState = downloadsReducer( @@ -196,15 +204,15 @@ describe("downloadedMessageAttachmentSelector", () => { }); it("Should return data for a matching downloaded attachment", () => { const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; - const attachmentId = "1" as UIAttachmentId; + const attachmentId = "1"; const downloadPath = "randomPath"; const successDownload = { attachment: { - messageId, id: attachmentId - } as UIAttachment, + } as ThirdPartyAttachment, + messageId, path: downloadPath - } as Download; + } as DownloadAttachmentSuccess; const downloadSuccessAction = downloadAttachment.success(successDownload); const downloadsState = downloadsReducer( INITIAL_STATE, @@ -224,13 +232,14 @@ describe("downloadedMessageAttachmentSelector", () => { ); expect(downloadedAttachment).toBeDefined(); expect(downloadedAttachment?.attachment).toBeDefined(); - expect(downloadedAttachment?.attachment.messageId).toBe(messageId); expect(downloadedAttachment?.attachment.id).toBe(attachmentId); expect(downloadedAttachment?.path).toBe(downloadPath); }); }); describe("downloadsReducer", () => { + const messageId = "01HP08KKPY65CBF4TRPHGJJ1GT" as UIMessageId; + describe("given no download", () => { const initialState = {}; @@ -240,7 +249,8 @@ describe("downloadsReducer", () => { const afterRequestState = downloadsReducer( initialState, downloadAttachment.request({ - ...attachment, + attachment, + messageId, skipMixpanelTrackingOnFailure: false }) ); @@ -248,9 +258,14 @@ describe("downloadsReducer", () => { it("then it returns pot.loading", () => { expect( pot.isLoading( - afterRequestState[attachment.messageId][attachment.id] ?? pot.none + afterRequestState[messageId]?.[attachment.id] ?? pot.none ) ).toBeTruthy(); + expect(afterRequestState.requestedDownload).toBeDefined(); + expect(afterRequestState.requestedDownload?.messageId).toBe(messageId); + expect(afterRequestState.requestedDownload?.attachmentId).toBe( + attachment.id + ); }); describe("and the request succeeds", () => { @@ -261,11 +276,19 @@ describe("downloadsReducer", () => { afterRequestState, downloadAttachment.success({ attachment, + messageId, path }) - )[attachment.messageId][attachment.id] ?? pot.none + )[messageId]?.[attachment.id] ?? pot.none ) ).toBeTruthy(); + expect(afterRequestState.requestedDownload).toBeDefined(); + expect(afterRequestState.requestedDownload?.messageId).toBe( + messageId + ); + expect(afterRequestState.requestedDownload?.attachmentId).toBe( + attachment.id + ); }); }); @@ -277,11 +300,19 @@ describe("downloadsReducer", () => { afterRequestState, downloadAttachment.failure({ attachment, + messageId, error: new Error() }) - )[attachment.messageId][attachment.id] ?? pot.none + )[messageId]?.[attachment.id] ?? pot.none ) ).toBeTruthy(); + expect(afterRequestState.requestedDownload).toBeDefined(); + expect(afterRequestState.requestedDownload?.messageId).toBe( + messageId + ); + expect(afterRequestState.requestedDownload?.attachmentId).toBe( + attachment.id + ); }); }); }); @@ -290,7 +321,7 @@ describe("downloadsReducer", () => { describe("given a downloaded attachment", () => { const attachment = mockPdfAttachment; const initialState: Downloads = { - [attachment.messageId]: { + [messageId]: { [attachment.id]: pot.some({ attachment, path }) } }; @@ -301,11 +332,709 @@ describe("downloadsReducer", () => { pot.isNone( downloadsReducer( initialState, - removeCachedAttachment({ attachment, path }) - )[attachment.messageId][attachment.id] ?? pot.none + removeCachedAttachment({ attachment, messageId, path }) + )[messageId]?.[attachment.id] ?? pot.none ) ).toBeTruthy(); + expect(initialState.requestedDownload).toBeUndefined(); }); }); }); + + describe("given a downloading attachment", () => { + const attachment = mockPdfAttachment; + const initialState = downloadsReducer( + undefined, + downloadAttachment.request({ + attachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + + expect(initialState.requestedDownload).toBeDefined(); + expect(initialState.requestedDownload?.messageId).toBe(messageId); + expect(initialState.requestedDownload?.attachmentId).toBe(attachment.id); + + it("Should return pot.none and clear the requestedDownload after a downloadAttachment.cancel action", () => { + const cancelState = downloadsReducer( + initialState, + downloadAttachment.cancel({ + attachment, + messageId + }) + ); + const potNone = cancelState[messageId]?.[attachment.id]; + expect(potNone).toBeDefined(); + expect(pot.isNone(potNone!)).toBeTruthy(); + expect(cancelState.requestedDownload).toBeUndefined(); + }); + it("Should clear the requestedDownload after a clearRequestedAttachmentDownload action", () => { + const cancelState = downloadsReducer( + initialState, + clearRequestedAttachmentDownload() + ); + expect(cancelState.requestedDownload).toBeUndefined(); + }); + }); + + describe("isDownloadingMessageAttachmentSelector", () => { + it("should return false on initial state", () => { + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return true on a matching download", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeTruthy(); + }); + it("should return false on a messageId-unmatching download", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + initialState, + "01HNWPGF3TY9WQYGX5JYAW816W" as UIMessageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return false on an attachmentId-unmatching download", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + initialState, + messageId, + "potato" + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return false on a successful downloaded attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const successState = appReducer( + initialState, + downloadAttachment.success({ + attachment: mockPdfAttachment, + messageId, + path: `file:///${mockPdfAttachment.id}.pdf` + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + successState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return false on a failed downloaded attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + failureState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return false on a cancelled downloaded attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const cancelledState = appReducer( + initialState, + downloadAttachment.cancel({ + attachment: mockPdfAttachment, + messageId + }) + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + cancelledState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeFalsy(); + }); + it("should return true on a cleared stated", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const clearedState = appReducer( + initialState, + clearRequestedAttachmentDownload() + ); + const isDownloadingMessage = isDownloadingMessageAttachmentSelector( + clearedState, + messageId, + mockPdfAttachment.id + ); + expect(isDownloadingMessage).toBeTruthy(); + }); + }); + + describe("hasErrorOccourredOnMessageAttachmentDownloadSelector", () => { + it("should return false on initial state", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(isError).toBeFalsy(); + }); + it("should return false on a messageId-unmatching attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + failureState, + "01HNWQ5YDG02JFGFH9523AC04Z" as UIMessageId, + mockPdfAttachment.id + ); + expect(isError).toBeFalsy(); + }); + it("should return false on an attachmentId-unmatching attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + failureState, + messageId, + "potato" + ); + expect(isError).toBeFalsy(); + }); + it("should return true on a failed attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + failureState, + messageId, + mockPdfAttachment.id + ); + expect(isError).toBeTruthy(); + }); + it("should return false on a successful attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.success({ + attachment: mockPdfAttachment, + messageId, + path: `file:///${mockPdfAttachment.id}.pdf` + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + failureState, + messageId, + mockPdfAttachment.id + ); + expect(isError).toBeFalsy(); + }); + it("should return false on a cancelled attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.cancel({ + attachment: mockPdfAttachment, + messageId + }) + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + failureState, + messageId, + mockPdfAttachment.id + ); + expect(isError).toBeFalsy(); + }); + it("should return false on a failed attachment after clear state", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const clearState = appReducer( + failureState, + clearRequestedAttachmentDownload() + ); + const isError = hasErrorOccourredOnRequestedDownloadSelector( + clearState, + messageId, + mockPdfAttachment.id + ); + expect(isError).toBeFalsy(); + }); + }); + + describe("downloadPotForMessageAttachmentSelector", () => { + it("should return pot.none on initial state", () => { + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isNone(downloadPot)).toBeTruthy(); + }); + it("should return pot.none on unmatching-messageId attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + initialState, + "01HNWR6BGZ3M8FN9Y61XS37K8C" as UIMessageId, + mockPdfAttachment.id + ); + expect(pot.isNone(downloadPot)).toBeTruthy(); + }); + it("should return pot.none on unmatching-attachmentId attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + initialState, + messageId, + "potato" + ); + expect(pot.isNone(downloadPot)).toBeTruthy(); + }); + it("should return pot.loading on a requested attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isLoading(downloadPot)).toBeTruthy(); + }); + it("should return pot.some on a successful attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const successfulState = appReducer( + initialState, + downloadAttachment.success({ + attachment: mockPdfAttachment, + messageId, + path: `file:///${mockPdfAttachment.id}.pdf` + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + successfulState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isSome(downloadPot)).toBeTruthy(); + }); + it("should return pot.error on a failed attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failureState = appReducer( + initialState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + failureState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isError(downloadPot)).toBeTruthy(); + }); + it("should return pot.error on a cancelled attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const cancelledState = appReducer( + initialState, + downloadAttachment.cancel({ + attachment: mockPdfAttachment, + messageId + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + cancelledState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isNone(downloadPot)).toBeTruthy(); + }); + it("should return pot.error on a removed cached attachment", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const cachedState = appReducer( + initialState, + removeCachedAttachment({ + attachment: mockPdfAttachment, + messageId, + path: `file:///${mockPdfAttachment.id}.pdf` + }) + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + cachedState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isNone(downloadPot)).toBeTruthy(); + }); + it("should return pot.some on a downloading attachment after clear requested download", () => { + const initialState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const clearRequestedState = appReducer( + initialState, + clearRequestedAttachmentDownload() + ); + const downloadPot = downloadPotForMessageAttachmentSelector( + clearRequestedState, + messageId, + mockPdfAttachment.id + ); + expect(pot.isLoading(downloadPot)).toBeTruthy(); + }); + }); + + describe("isRequestedAttachmentDownloadSelector", () => { + it("should return false on initial state", () => { + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + initialState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeFalsy(); + }); + it("should return true on matching downloading attachment", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + downloadingAttachmentState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeTruthy(); + }); + it("should return false on an messageId-unmatching downloading attachment", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + downloadingAttachmentState, + "01HNWNXS6G2Y86HEFQ3AYSQA1Q" as UIMessageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeFalsy(); + }); + it("should return false on an attachmentId-unmatching downloading attachment", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + downloadingAttachmentState, + messageId, + "potato" + ); + expect(isRequestedAttachmentDownload).toBeFalsy(); + }); + it("should return true on successful downloaded attachment", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const successfulDownloadState = appReducer( + downloadingAttachmentState, + downloadAttachment.success({ + attachment: mockPdfAttachment, + messageId, + path: `file:///${mockPdfAttachment.id}.pdf` + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + successfulDownloadState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeTruthy(); + }); + it("should return true on failed downloaded attachment", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const failedDownloadState = appReducer( + downloadingAttachmentState, + downloadAttachment.failure({ + attachment: mockPdfAttachment, + messageId, + error: new Error("") + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + failedDownloadState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeTruthy(); + }); + it("should return false on matching cancelled attachment download", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const cancelledDownloadState = appReducer( + downloadingAttachmentState, + downloadAttachment.cancel({ + attachment: mockPdfAttachment, + messageId + }) + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + cancelledDownloadState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeFalsy(); + }); + it("should return false on clear requested download", () => { + const downloadingAttachmentState = appReducer( + undefined, + downloadAttachment.request({ + attachment: mockPdfAttachment, + messageId, + skipMixpanelTrackingOnFailure: false + }) + ); + const clearRequestedDownloadState = appReducer( + downloadingAttachmentState, + clearRequestedAttachmentDownload() + ); + const isRequestedAttachmentDownload = + isRequestedAttachmentDownloadSelector( + clearRequestedDownloadState, + messageId, + mockPdfAttachment.id + ); + expect(isRequestedAttachmentDownload).toBeFalsy(); + }); + }); }); diff --git a/ts/features/messages/store/reducers/__tests__/payments.test.ts b/ts/features/messages/store/reducers/__tests__/payments.test.ts new file mode 100644 index 00000000000..53dff02e290 --- /dev/null +++ b/ts/features/messages/store/reducers/__tests__/payments.test.ts @@ -0,0 +1,1547 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { Detail_v2Enum } from "../../../../../../definitions/backend/PaymentProblemJson"; +import { PaymentRequestsGetResponse } from "../../../../../../definitions/backend/PaymentRequestsGetResponse"; +import { reloadAllMessages } from "../../../../messages/store/actions"; +import { Action } from "../../../../../store/actions/types"; +import { appReducer } from "../../../../../store/reducers"; +import { + PaymentData, + UIMessageDetails, + UIMessageId +} from "../../../../messages/types"; +import { + remoteError, + remoteLoading, + remoteReady, + remoteUndefined +} from "../../../../../common/model/RemoteValue"; +import { + addUserSelectedPaymentRptId, + updatePaymentForMessage +} from "../../actions"; +import { + initialState, + paymentStatusForUISelector, + userSelectedPaymentRptIdSelector, + paymentsReducer, + shouldUpdatePaymentSelector, + isUserSelectedPaymentSelector, + canNavigateToPaymentFromMessageSelector, + paymentsButtonStateSelector, + isPaymentsButtonVisibleSelector, + paymentExpirationBannerStateSelector +} from "../payments"; +import { getRptIdStringFromPaymentData } from "../../../utils"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import * as versionInfo from "../../../../../common/versionInfo/store/reducers/versionInfo"; +import * as profile from "../../../../../store/reducers/profile"; +import { GlobalState } from "../../../../../store/reducers/types"; + +describe("Messages payments reducer's tests", () => { + it("Should match initial state upon initialization", () => { + const firstState = paymentsReducer(undefined, {} as Action); + expect(firstState).toEqual(initialState); + }); + it("Should have undefined value for an undefined Message Id", () => { + const requestAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const paymentsState = paymentsReducer(undefined, requestAction); + const unknownMessageId = "m2" as UIMessageId; + const messageState = paymentsState[unknownMessageId]; + expect(messageState).toBeUndefined(); + }); + it("Should have undefined value for an unknown paymentId", () => { + const messageId = "m1" as UIMessageId; + const requestAction = updatePaymentForMessage.request({ + messageId, + paymentId: "p1" + }); + const paymentsState = paymentsReducer(undefined, requestAction); + const messageState = paymentsState[messageId]; + expect(messageState).toBeTruthy(); + const unknownPaymentId = "p2"; + const paymentState = messageState?.[unknownPaymentId]; + expect(paymentState).toBeUndefined(); + }); + it("Should have remoteLoading value for a updatePaymentForMessage.request", () => { + const messageId = "m1" as UIMessageId; + const paymentId = "p1"; + const requestAction = updatePaymentForMessage.request({ + messageId, + paymentId + }); + const paymentsState = paymentsReducer(undefined, requestAction); + const messageState = paymentsState[messageId]; + expect(messageState).toBeTruthy(); + const paymentState = messageState?.[paymentId]; + expect(paymentState).toBe(remoteLoading); + }); + it("Should have remoteReady value for a updatePaymentForMessage.success", () => { + const messageId = "m1" as UIMessageId; + const paymentId = "p1"; + const requestAction = updatePaymentForMessage.request({ + messageId, + paymentId + }); + const paymentsState = paymentsReducer(undefined, requestAction); + const paymentData = { + importoSingoloVersamento: 100, + codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + } as PaymentRequestsGetResponse; + const successAction = updatePaymentForMessage.success({ + messageId, + paymentId, + paymentData + }); + const updatedPaymentsState = paymentsReducer(paymentsState, successAction); + const messageState = updatedPaymentsState[messageId]; + expect(messageState).toBeTruthy(); + const paymentState = messageState?.[paymentId]; + const remoteSuccessPaymentData = remoteReady(paymentData); + expect(paymentState).toStrictEqual(remoteSuccessPaymentData); + }); + it("Should have remoteError value for a updatePaymentForMessage.failure", () => { + const messageId = "m1" as UIMessageId; + const paymentId = "p1"; + const requestAction = updatePaymentForMessage.request({ + messageId, + paymentId + }); + const paymentsState = paymentsReducer(undefined, requestAction); + const details = Detail_v2Enum.CANALE_BUSTA_ERRATA; + const failureAction = updatePaymentForMessage.failure({ + messageId, + paymentId, + details + }); + const updatedPaymentsState = paymentsReducer(paymentsState, failureAction); + const messageState = updatedPaymentsState[messageId]; + expect(messageState).toBeTruthy(); + const paymentState = messageState?.[paymentId]; + const remoteSuccessPaymentData = remoteError(details); + expect(paymentState).toStrictEqual(remoteSuccessPaymentData); + }); + it("Should handle multiple payments for a single message", () => { + const messageId = "m1" as UIMessageId; + const paymentId1 = "p1"; + const requestAction = updatePaymentForMessage.request({ + messageId, + paymentId: paymentId1 + }); + const firstStateGeneration = paymentsReducer(undefined, requestAction); + const paymentId2 = "p2"; + const secondPaymentData = { + importoSingoloVersamento: 100, + codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + } as PaymentRequestsGetResponse; + const successAction = updatePaymentForMessage.success({ + messageId, + paymentId: paymentId2, + paymentData: secondPaymentData + }); + const secondStateGeneration = paymentsReducer( + firstStateGeneration, + successAction + ); + const paymentId3 = "p3"; + const thirdPaymentDetails = Detail_v2Enum.CANALE_BUSTA_ERRATA; + const failureAction = updatePaymentForMessage.failure({ + messageId, + paymentId: paymentId3, + details: thirdPaymentDetails + }); + const finalStateGeneration = paymentsReducer( + secondStateGeneration, + failureAction + ); + const messageState = finalStateGeneration[messageId]; + expect(messageState).toBeTruthy(); + const firstPaymentState = messageState?.[paymentId1]; + expect(firstPaymentState).toBe(remoteLoading); + const secondPaymentState = messageState?.[paymentId2]; + expect(secondPaymentState).toStrictEqual(remoteReady(secondPaymentData)); + const thirdPaymentState = messageState?.[paymentId3]; + expect(thirdPaymentState).toStrictEqual(remoteError(thirdPaymentDetails)); + }); + it("Should handle multiple payments for multiple messages", () => { + const messageId1 = "m1" as UIMessageId; + const paymentId1 = "p1"; + const requestAction = updatePaymentForMessage.request({ + messageId: messageId1, + paymentId: paymentId1 + }); + const firstStateGeneration = paymentsReducer(undefined, requestAction); + const messageId2 = "m2" as UIMessageId; + const successfulPaymentData = { + importoSingoloVersamento: 100, + codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + } as PaymentRequestsGetResponse; + const successAction = updatePaymentForMessage.success({ + messageId: messageId2, + paymentId: paymentId1, + paymentData: successfulPaymentData + }); + const secondStateGeneration = paymentsReducer( + firstStateGeneration, + successAction + ); + const messageId3 = "m3" as UIMessageId; + const failedPaymentDetails = Detail_v2Enum.CANALE_BUSTA_ERRATA; + const failureAction = updatePaymentForMessage.failure({ + messageId: messageId3, + paymentId: paymentId1, + details: failedPaymentDetails + }); + const finalStateGeneration = paymentsReducer( + secondStateGeneration, + failureAction + ); + const message1State = finalStateGeneration[messageId1]; + expect(message1State).toBeTruthy(); + const firstPaymentState = message1State?.[paymentId1]; + expect(firstPaymentState).toBe(remoteLoading); + const message2State = finalStateGeneration[messageId2]; + expect(message2State).toBeTruthy(); + const secondPaymentState = message2State?.[paymentId1]; + expect(secondPaymentState).toStrictEqual( + remoteReady(successfulPaymentData) + ); + const message3State = finalStateGeneration[messageId3]; + expect(message3State).toBeTruthy(); + const thirdPaymentState = message3State?.[paymentId1]; + expect(thirdPaymentState).toStrictEqual(remoteError(failedPaymentDetails)); + }); + it("Should remove payment statuses on updatePaymentForMessage.cancel", () => { + const messageId1 = "m1" as UIMessageId; + const paymentId1 = "p1"; + const requestAction1 = updatePaymentForMessage.request({ + messageId: messageId1, + paymentId: paymentId1 + }); + const firstStateGeneration = paymentsReducer(undefined, requestAction1); + const messageId2 = "m2" as UIMessageId; + const requestAction2 = updatePaymentForMessage.request({ + messageId: messageId2, + paymentId: paymentId1 + }); + const secondStateGeneration = paymentsReducer( + firstStateGeneration, + requestAction2 + ); + const paymentId2 = "p2"; + const requestAction3 = updatePaymentForMessage.request({ + messageId: messageId2, + paymentId: paymentId2 + }); + const thirdStateGeneration = paymentsReducer( + secondStateGeneration, + requestAction3 + ); + const messageId3 = "m3" as UIMessageId; + const requestAction4 = updatePaymentForMessage.request({ + messageId: messageId3, + paymentId: paymentId1 + }); + const fourthStateGeneration = paymentsReducer( + thirdStateGeneration, + requestAction4 + ); + const requestAction5 = updatePaymentForMessage.request({ + messageId: messageId3, + paymentId: paymentId2 + }); + const fifthStateGeneration = paymentsReducer( + fourthStateGeneration, + requestAction5 + ); + const paymentId3 = "p3"; + const requestAction6 = updatePaymentForMessage.request({ + messageId: messageId3, + paymentId: paymentId3 + }); + const sixthStateGeneration = paymentsReducer( + fifthStateGeneration, + requestAction6 + ); + + const m1S1 = sixthStateGeneration[messageId1]; + expect(m1S1).toBeTruthy(); + const m1p1S1 = m1S1?.[paymentId1]; + expect(m1p1S1).toStrictEqual(remoteLoading); + + const m2S1 = sixthStateGeneration[messageId2]; + expect(m2S1).toBeTruthy(); + const m2p1S1 = m2S1?.[paymentId1]; + expect(m2p1S1).toStrictEqual(remoteLoading); + const m2p2S1 = m2S1?.[paymentId2]; + expect(m2p2S1).toStrictEqual(remoteLoading); + + const m3S1 = sixthStateGeneration[messageId3]; + expect(m3S1).toBeTruthy(); + const m3p1S1 = m3S1?.[paymentId1]; + expect(m3p1S1).toStrictEqual(remoteLoading); + const m3p2S1 = m3S1?.[paymentId2]; + expect(m3p2S1).toStrictEqual(remoteLoading); + const m3p3S1 = m3S1?.[paymentId3]; + expect(m3p3S1).toStrictEqual(remoteLoading); + + const cancelPaymentAction = updatePaymentForMessage.cancel([ + { + messageId: messageId1, + paymentId: paymentId1 + }, + { + messageId: messageId2, + paymentId: paymentId2 + }, + { + messageId: messageId3, + paymentId: paymentId2 + }, + { + messageId: messageId3, + paymentId: paymentId3 + } + ]); + const finalStateGeneration = paymentsReducer( + sixthStateGeneration, + cancelPaymentAction + ); + + const m1S2 = finalStateGeneration[messageId1]; + expect(m1S2).toBeTruthy(); + const m1p1S2 = m1S2?.[paymentId1]; + expect(m1p1S2).toBeUndefined(); + + const m2S2 = finalStateGeneration[messageId2]; + expect(m2S2).toBeTruthy(); + const m2p1S2 = m2S2?.[paymentId1]; + expect(m2p1S2).toStrictEqual(remoteLoading); + const m2p2S2 = m2S2?.[paymentId2]; + expect(m2p2S2).toBeUndefined(); + + const m3S2 = finalStateGeneration[messageId3]; + expect(m3S2).toBeTruthy(); + const m3p1S2 = m3S2?.[paymentId1]; + expect(m3p1S2).toStrictEqual(remoteLoading); + const m3p2S2 = m3S2?.[paymentId2]; + expect(m3p2S2).toBeUndefined(); + const m3p3S2 = m3S2?.[paymentId3]; + expect(m3p3S2).toBeUndefined(); + }); + it("Should have the paymentId for an addUserSelectedPaymentRptId action", () => { + const paymentId = "p1"; + const setSelectedPaymentAction = addUserSelectedPaymentRptId(paymentId); + const paymentsState = paymentsReducer(undefined, setSelectedPaymentAction); + const userSelectedPayments = paymentsState.userSelectedPayments; + const hasPaymentRptId = userSelectedPayments.has(paymentId); + expect(hasPaymentRptId).toBe(true); + }); + it("Should clear the paymentId for a updatePaymentForMessage.request action", () => { + const paymentId = "p1"; + const setSelectedPaymentAction = addUserSelectedPaymentRptId(paymentId); + const startingPaymentsState = paymentsReducer( + undefined, + setSelectedPaymentAction + ); + const userSelectedPayments = startingPaymentsState.userSelectedPayments; + const hasPaymentRptId = userSelectedPayments.has(paymentId); + expect(hasPaymentRptId).toBe(true); + const endingPaymentsState = paymentsReducer( + startingPaymentsState, + updatePaymentForMessage.request({ + messageId: "01HR9GY9GHGH5BQEJAKPWXEKV3" as UIMessageId, + paymentId + }) + ); + const endingUserSelectedPayments = endingPaymentsState.userSelectedPayments; + const endingHasPaymentRptId = endingUserSelectedPayments.has(paymentId); + expect(endingHasPaymentRptId).toBe(false); + }); + it("Should clear the paymentId for a reloadAllMessages action", () => { + const paymentId = "p1"; + const addMessagePaymentToCheckAction = + addUserSelectedPaymentRptId(paymentId); + const startingPaymentsState = paymentsReducer( + undefined, + addMessagePaymentToCheckAction + ); + const startingUserSelectedPayments = + startingPaymentsState.userSelectedPayments; + const startingHasPaymentToCheck = + startingUserSelectedPayments.has(paymentId); + expect(startingHasPaymentToCheck).toBe(true); + const endingPaymentsState = paymentsReducer( + startingPaymentsState, + reloadAllMessages.request({ pageSize: 12, filter: {} }) + ); + const endingUserSelectedPayments = endingPaymentsState.userSelectedPayments; + const endingPaymentsToCheckSize = endingUserSelectedPayments.size; + expect(endingPaymentsToCheckSize).toBe(0); + }); +}); + +describe("PN Payments selectors' tests", () => { + it("shouldUpdatePaymentSelector should return true for an unmatching message Id", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const shouldUpdatePayment = shouldUpdatePaymentSelector( + state, + "m2" as UIMessageId, + "p1" + ); + expect(shouldUpdatePayment).toBeTruthy(); + }); + it("shouldUpdatePaymentSelector should return true for a matching message Id with an unmatching payment Id", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const shouldUpdatePayment = shouldUpdatePaymentSelector( + state, + "m1" as UIMessageId, + "p2" + ); + expect(shouldUpdatePayment).toBeTruthy(); + }); + it("shouldUpdatePaymentSelector should return false for a matching pair", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const shouldUpdatePayment = shouldUpdatePaymentSelector( + state, + "m1" as UIMessageId, + "p1" + ); + expect(shouldUpdatePayment).toBeFalsy(); + }); + it("paymentStatusForUISelector should return remoteUndefined for an unmatching message Id", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const paymentStatus = paymentStatusForUISelector( + state, + "m2" as UIMessageId, + "p1" + ); + expect(paymentStatus).toBe(remoteUndefined); + }); + it("paymentStatusForUISelector should return remoteUndefined for a matching message Id with an unmatching payment Id", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const paymentStatus = paymentStatusForUISelector( + state, + "m1" as UIMessageId, + "p2" + ); + expect(paymentStatus).toBe(remoteUndefined); + }); + it("paymentStatusForUISelector should return remoteUndefined for a matching that is loading", () => { + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const paymentStatusOnStore = + state.entities.messages.payments["m1" as UIMessageId]?.p1; + expect(paymentStatusOnStore).toBe(remoteLoading); + const paymentStatus = paymentStatusForUISelector( + state, + "m1" as UIMessageId, + "p1" + ); + expect(paymentStatus).toBe(remoteUndefined); + }); + it("paymentStatusForUISelector should return remoteReady for a matching that is payable", () => { + const paymentData = {} as PaymentRequestsGetResponse; + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.success({ + messageId: "m1" as UIMessageId, + paymentId: "p1", + paymentData + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const paymentStatus = paymentStatusForUISelector( + state, + "m1" as UIMessageId, + "p1" + ); + expect(paymentStatus).toStrictEqual(remoteReady(paymentData)); + }); + it("paymentStatusForUISelector should return remoteError for a matching that is not payable anymore", () => { + const details = Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO; + const startingState = appReducer(undefined, {} as Action); + const updatePaymentForMessageAction = updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "p1", + details + }); + const state = appReducer(startingState, updatePaymentForMessageAction); + const paymentStatus = paymentStatusForUISelector( + state, + "m1" as UIMessageId, + "p1" + ); + expect(paymentStatus).toStrictEqual(remoteError(details)); + }); + + it("addUserSelectedPaymentRptId should contain added user selected payments and removed one later", () => { + const paymentId1 = "01234567890012345678912345610"; + const paymentId2 = "01234567890012345678912345620"; + const paymentId3 = "01234567890012345678912345630"; + const addAction1 = addUserSelectedPaymentRptId(paymentId1); + const addAction2 = addUserSelectedPaymentRptId(paymentId2); + const addAction3 = addUserSelectedPaymentRptId(paymentId3); + const initialAppState = appReducer(undefined, addAction1); + const intermediateAppState = appReducer(initialAppState, addAction2); + const finalAppState = appReducer(intermediateAppState, addAction3); + const userSelectedPayments1 = + finalAppState.entities.messages.payments.userSelectedPayments; + expect(userSelectedPayments1.has(paymentId1)).toBe(true); + expect(userSelectedPayments1.has(paymentId2)).toBe(true); + expect(userSelectedPayments1.has(paymentId3)).toBe(true); + const firstRemovedAppState = appReducer( + finalAppState, + updatePaymentForMessage.request({ + messageId: "" as UIMessageId, + paymentId: paymentId1 + }) + ); + const userSelectedPayments2 = + firstRemovedAppState.entities.messages.payments.userSelectedPayments; + expect(userSelectedPayments2.has(paymentId1)).toBe(false); + expect(userSelectedPayments2.has(paymentId2)).toBe(true); + expect(userSelectedPayments2.has(paymentId3)).toBe(true); + const secondRemovedAppState = appReducer( + firstRemovedAppState, + updatePaymentForMessage.request({ + messageId: "" as UIMessageId, + paymentId: paymentId2 + }) + ); + const userSelectedPayments3 = + secondRemovedAppState.entities.messages.payments.userSelectedPayments; + expect(userSelectedPayments3.has(paymentId1)).toBe(false); + expect(userSelectedPayments3.has(paymentId2)).toBe(false); + expect(userSelectedPayments3.has(paymentId3)).toBe(true); + const thirdRemovedAppState = appReducer( + secondRemovedAppState, + updatePaymentForMessage.request({ + messageId: "" as UIMessageId, + paymentId: paymentId3 + }) + ); + const userSelectedPayments4 = + thirdRemovedAppState.entities.messages.payments.userSelectedPayments; + expect(userSelectedPayments4.has(paymentId1)).toBe(false); + expect(userSelectedPayments4.has(paymentId2)).toBe(false); + expect(userSelectedPayments4.has(paymentId3)).toBe(false); + }); +}); + +describe("isUserSelectedPaymentSelector", () => { + it("should return false when there is no match on an empty state", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const isUserSelectedPayment = isUserSelectedPaymentSelector( + appState, + "01234567890012345678912345610" + ); + expect(isUserSelectedPayment).toBe(false); + }); + it("should return false when there is no match on a non-empty state", () => { + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const appState = appReducer( + initialState, + addUserSelectedPaymentRptId("01234567890012345678912345620") + ); + const isUserSelectedPayment = isUserSelectedPaymentSelector( + appState, + "01234567890012345678912345610" + ); + expect(isUserSelectedPayment).toBe(false); + }); + it("should return true when there is a match", () => { + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const intermediateState = appReducer( + initialState, + addUserSelectedPaymentRptId("01234567890012345678912345620") + ); + const rptId = "01234567890012345678912345610"; + const appState = appReducer( + intermediateState, + addUserSelectedPaymentRptId(rptId) + ); + const isUserSelectedPayment = isUserSelectedPaymentSelector( + appState, + rptId + ); + expect(isUserSelectedPayment).toBe(true); + }); +}); + +describe("userSelectedPaymentRptIdSelector", () => { + it("should return undefined when none is set", () => { + const appState = appReducer(undefined, {} as Action); + const messageDetails = { + paymentData: { + noticeNumber: "", + payee: { + fiscalCode: "" + } + } + } as UIMessageDetails; + const paymentToCheckRptId = userSelectedPaymentRptIdSelector( + appState, + messageDetails + ); + expect(paymentToCheckRptId).toBeUndefined(); + }); + it("should return none when ids do not match", () => { + const paymentData = { + noticeNumber: "012345678912345678", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetails = { + paymentData: { + noticeNumber: "012345678912345679", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData + } as UIMessageDetails; + const rtpId = getRptIdStringFromPaymentData(paymentData); + const appState = appReducer(undefined, addUserSelectedPaymentRptId(rtpId)); + const paymentToCheckRptId = userSelectedPaymentRptIdSelector( + appState, + messageDetails + ); + expect(paymentToCheckRptId).toBeUndefined(); + }); + it("should return the selected payment when it matches", () => { + const paymentData = { + noticeNumber: "012345678912345678", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetails = { + paymentData + } as UIMessageDetails; + const rtpId = getRptIdStringFromPaymentData(paymentData); + const appState = appReducer(undefined, addUserSelectedPaymentRptId(rtpId)); + const paymentToCheckRptId = userSelectedPaymentRptIdSelector( + appState, + messageDetails + ); + expect(paymentToCheckRptId).toBe(rtpId); + }); + it("should return the selected payment when it matches (and there are multiple user selected payments)", () => { + const paymentData = { + noticeNumber: "012345678912345678", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetails = { + paymentData + } as UIMessageDetails; + const appState = appReducer( + undefined, + addUserSelectedPaymentRptId( + getRptIdStringFromPaymentData({ + noticeNumber: "012345678912345677", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData) + ) + ); + const appStateIntermediate = appReducer( + appState, + addUserSelectedPaymentRptId( + getRptIdStringFromPaymentData({ + noticeNumber: "012345678912345676", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData) + ) + ); + const rtpId = getRptIdStringFromPaymentData(paymentData); + const appStateFinal = appReducer( + appStateIntermediate, + addUserSelectedPaymentRptId(rtpId) + ); + const paymentToCheckRptId = userSelectedPaymentRptIdSelector( + appStateFinal, + messageDetails + ); + expect(paymentToCheckRptId).toBe(rtpId); + }); +}); + +describe("canNavigateToPaymentFromMessageSelector", () => { + it("should return false if profile email is not validated and pagopa is not supported", () => { + jest + .spyOn(profile, "isProfileEmailValidatedSelector") + .mockReturnValueOnce(false); + jest + .spyOn(versionInfo, "isPagoPaSupportedSelector") + .mockReturnValueOnce(false); + + const appState = appReducer(undefined, applicationChangeState("active")); + const canNavigateToPaymentFromMessage = + canNavigateToPaymentFromMessageSelector(appState); + expect(canNavigateToPaymentFromMessage).toBe(false); + }); + it("should return false if profile email is not validated", () => { + jest + .spyOn(profile, "isProfileEmailValidatedSelector") + .mockReturnValueOnce(false); + jest + .spyOn(versionInfo, "isPagoPaSupportedSelector") + .mockReturnValueOnce(true); + + const appState = appReducer(undefined, applicationChangeState("active")); + const canNavigateToPaymentFromMessage = + canNavigateToPaymentFromMessageSelector(appState); + expect(canNavigateToPaymentFromMessage).toBe(false); + }); + it("should return false if pagopa is not supported", () => { + jest + .spyOn(profile, "isProfileEmailValidatedSelector") + .mockReturnValueOnce(true); + jest + .spyOn(versionInfo, "isPagoPaSupportedSelector") + .mockReturnValueOnce(false); + + const appState = appReducer(undefined, applicationChangeState("active")); + const canNavigateToPaymentFromMessage = + canNavigateToPaymentFromMessageSelector(appState); + expect(canNavigateToPaymentFromMessage).toBe(false); + }); + it("should return true if email si validated and pagopa is supported", () => { + jest + .spyOn(profile, "isProfileEmailValidatedSelector") + .mockReturnValueOnce(true); + jest + .spyOn(versionInfo, "isPagoPaSupportedSelector") + .mockReturnValueOnce(true); + + const appState = appReducer(undefined, applicationChangeState("active")); + const canNavigateToPaymentFromMessage = + canNavigateToPaymentFromMessageSelector(appState); + expect(canNavigateToPaymentFromMessage).toBe(true); + }); +}); + +describe("paymentsButtonStateSelector", () => { + it("should return hidden for a pot.none message details", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentsButtonState = paymentsButtonStateSelector( + appState, + messageId + ); + expect(paymentsButtonState).toBe("hidden"); + }); + it("should return hidden for a message without payment data", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const messageDetailsPot = pot.some({ + id: messageId + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("hidden"); + }); + it("should return hidden for a payment with an error", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": { + "01234567890012345678912345610": remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ) + } + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("hidden"); + }); + it("should return loading for a payment with no data (no message entry in the payment section of redux)", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("loading"); + }); + it("should return loading for a payment with no data (no payment entry in the message's payment section of redux)", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": {} + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("loading"); + }); + it("should return loading for a payment with remoteUndefined value", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": { + "01234567890012345678912345610": remoteUndefined + } + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("loading"); + }); + it("should return loading for a loading payment", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": { + "01234567890012345678912345610": remoteLoading + } + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("loading"); + }); + it("should return enabled for a payable payment", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const paymentData = { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData; + const messageDetailsPot = pot.some({ + id: messageId, + paymentData + } as UIMessageDetails); + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + "01HRSSD1R29DA2HJQHGYJP19T8": messageDetailsPot + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": { + "01234567890012345678912345610": remoteReady({}) + } + } + } + } + } as GlobalState; + const paymentsButtonState = paymentsButtonStateSelector( + finalState, + messageId + ); + expect(paymentsButtonState).toBe("enabled"); + }); +}); + +describe("isPaymentsButtonVisibleSelector", () => { + it("Should return false when the button is hidden", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const isPaymentButtonVisible = isPaymentsButtonVisibleSelector( + appState, + messageId + ); + expect(isPaymentButtonVisible).toBe(false); + }); + it("Should return true when the button is loading", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + "01HRSSD1R29DA2HJQHGYJP19T8": pot.some({ + id: messageId, + paymentData: { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData + } as UIMessageDetails) + } + } + } + } as GlobalState; + const isPaymentButtonVisible = isPaymentsButtonVisibleSelector( + finalState, + messageId + ); + expect(isPaymentButtonVisible).toBe(true); + }); + it("Should return true when the button is enabled", () => { + const messageId = "01HRSSD1R29DA2HJQHGYJP19T8" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + "01HRSSD1R29DA2HJQHGYJP19T8": pot.some({ + id: messageId, + paymentData: { + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } as PaymentData + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + "01HRSSD1R29DA2HJQHGYJP19T8": { + "01234567890012345678912345610": remoteReady({}) + } + } + } + } + } as GlobalState; + const isPaymentButtonVisible = isPaymentsButtonVisibleSelector( + finalState, + messageId + ); + expect(isPaymentButtonVisible).toBe(true); + }); +}); + +describe("paymentExpirationBannerStateSelector", () => { + it("should return hidden when there is no message (undefined)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const status = paymentExpirationBannerStateSelector(appState, messageId); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is no message (pot.none)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.none + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is no message (pot.noneLoading)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.noneLoading + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is no message (pot.noneUpdating)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.noneUpdating({} as UIMessageDetails) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is no message (pot.noneError)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.noneError(new Error("")) as pot.Pot< + UIMessageDetails, + Error + > + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is a message without a due date (pot.some)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({} as UIMessageDetails) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is a message without a due date (pot.someLoading)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.someLoading({} as UIMessageDetails) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is a message without a due date (pot.someUpdating)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.someUpdating( + {} as UIMessageDetails, + {} as UIMessageDetails + ) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is a message without a due date (pot.someError)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.someError({}, new Error("")) as pot.Pot< + UIMessageDetails, + Error + > + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return hidden when there is a message with a due date but no payment data", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date() + } as UIMessageDetails) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); + it("should return loading when there is a message (pot.some) with a due date, payment data but no entry for the updated payment", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber: "012345678912345610", + payee: { + fiscalCode: "01234567890" + } + } + } as UIMessageDetails) + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("loading"); + }); + it("should return loading when there is a message (pot.some) with a due date, payment data but the updated payment is remoteUndefined", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const rptId = `${noticeNumber}${fiscalCode}`; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber, + payee: { + fiscalCode + } + } + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + [messageId]: { + [rptId]: remoteUndefined + } + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("loading"); + }); + it("should return loading when there is a message (pot.some) with a due date, payment data but the updated payment is remoteLoading", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const rptId = `${noticeNumber}${fiscalCode}`; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber, + payee: { + fiscalCode + } + } + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + [messageId]: { + [rptId]: remoteLoading + } + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("loading"); + }); + it("should return visibleExpiring when there is a message (pot.some) with a due date, payment data and the updated payment is remoteReady", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const rptId = `${fiscalCode}${noticeNumber}`; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber, + payee: { + fiscalCode + } + } + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + [messageId]: { + [rptId]: remoteReady({}) + } + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("visibleExpiring"); + }); + it("should return visibleExpired when there is a message (pot.some) with a due date, payment data and the updated payment is expired (remoteError(PAA_PAGAMENTO_SCADUTO))", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const rptId = `${fiscalCode}${noticeNumber}`; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber, + payee: { + fiscalCode + } + } + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + [messageId]: { + [rptId]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO) + } + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("visibleExpired"); + }); + it("should return hidden when there is a message (pot.some) with a due date, payment data and the updated payment has an error that is not remoteError(PAA_PAGAMENTO_SCADUTO)", () => { + const messageId = "01HSEBPZ2NW3K6ZV7ZEV8KZQC4" as UIMessageId; + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const rptId = `${fiscalCode}${noticeNumber}`; + const appState = appReducer(undefined, applicationChangeState("active")); + const enhancedState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + detailsById: { + ...appState.entities.messages.detailsById, + [messageId]: pot.some({ + dueDate: new Date(), + paymentData: { + amount: 199, + noticeNumber, + payee: { + fiscalCode + } + } + } as UIMessageDetails) + }, + payments: { + ...appState.entities.messages.payments, + [messageId]: { + [rptId]: remoteError(Detail_v2Enum.CANALE_CONVENZIONE_NON_VALIDA) + } + } + } + } + } as GlobalState; + const status = paymentExpirationBannerStateSelector( + enhancedState, + messageId + ); + expect(status).toBe("hidden"); + }); +}); diff --git a/ts/features/messages/store/reducers/__tests__/thirdPartyById.test.ts b/ts/features/messages/store/reducers/__tests__/thirdPartyById.test.ts index 98a572d8fc8..7e87b302461 100644 --- a/ts/features/messages/store/reducers/__tests__/thirdPartyById.test.ts +++ b/ts/features/messages/store/reducers/__tests__/thirdPartyById.test.ts @@ -1,17 +1,26 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; +import { Action } from "redux"; import { appReducer } from "../../../../../store/reducers"; import { ThirdPartyMessageWithContent } from "../../../../../../definitions/backend/ThirdPartyMessageWithContent"; import { loadMessageDetails, loadThirdPartyMessage } from "../../actions"; import { applicationChangeState } from "../../../../../store/actions/application"; import { + hasAttachmentsSelector, isThirdPartyMessageSelector, messageMarkdownSelector, messageTitleSelector, - thirdPartyFromIdSelector + thirdPartyFromIdSelector, + thirdPartyMessageAttachments } from "../thirdPartyById"; import { UIMessageDetails, UIMessageId } from "../../../types"; -import { ThirdPartyMessageDetails } from "../../../../../../definitions/backend/ThirdPartyMessage"; +import { + ThirdPartyMessage, + ThirdPartyMessageDetails +} from "../../../../../../definitions/backend/ThirdPartyMessage"; import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { ThirdPartyAttachment } from "../../../../../../definitions/backend/ThirdPartyAttachment"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { reproduceSequence } from "../../../../../utils/tests"; describe("thirdPartyFromIdSelector", () => { it("Should return pot none for an unmatching message id", () => { @@ -168,6 +177,37 @@ describe("messageTitleSelector", () => { const messageTitle = messageTitleSelector(finalState, messageId); expect(messageTitle).toBe(thirdPartySubject); }); + it("should return the message title when we have the detail and third party message with bad typed details", () => { + const messageId = "m1" as UIMessageId; + const content = { + id: messageId as string, + third_party_message: { + details: { + randomProperty: 5 + } as ThirdPartyMessageDetails + } + } as ThirdPartyMessageWithContent; + const detailsSubject = "message subject"; + + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageDetails.success({ + id: messageId, + subject: detailsSubject + } as UIMessageDetails), + loadThirdPartyMessage.success({ + id: messageId, + content + }) + ]; + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const messageTitle = messageTitleSelector(state, messageId); + expect(messageTitle).toBe(detailsSubject); + }); }); describe("messageMarkdownSelector", () => { @@ -228,7 +268,7 @@ describe("messageMarkdownSelector", () => { const messageMarkdown = messageMarkdownSelector(state, messageId); expect(messageMarkdown).toBeUndefined(); }); - it("Should return the message title for a matching loaded third party message with proper typed details", () => { + it("Should return the message markdown for a matching loaded third party message with proper typed details", () => { const messageId = "m1" as UIMessageId; const markdown = "This is a more than 80 characters message markdown length. The decoder needs this"; @@ -249,7 +289,7 @@ describe("messageMarkdownSelector", () => { const messageMarkdown = messageMarkdownSelector(state, messageId); expect(messageMarkdown).toBe(markdown); }); - it("Should return the third party message title when there are both detailed and third party message", () => { + it("Should return the third party message markdown when there are both detailed and third party message", () => { const messageId = "m1" as UIMessageId; const thirdPartyMarkdown = "This is a more than 80 characters message markdown length. The decoder needs this"; @@ -276,4 +316,155 @@ describe("messageMarkdownSelector", () => { const messageMarkdown = messageMarkdownSelector(finalState, messageId); expect(messageMarkdown).toBe(thirdPartyMarkdown); }); + it("should return the message markdown when we have the detail and third-party message with bad typed details", () => { + const messageId = "m1" as UIMessageId; + const content = { + id: messageId as string, + third_party_message: { + details: { + randomProperty: 5 + } as ThirdPartyMessageDetails + } + } as ThirdPartyMessageWithContent; + const detailsMarkdown = "message markdown"; + + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadMessageDetails.success({ + id: messageId, + subject: detailsMarkdown + } as UIMessageDetails), + loadThirdPartyMessage.success({ + id: messageId, + content + }) + ]; + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const messageTitle = messageTitleSelector(state, messageId); + expect(messageTitle).toBe(detailsMarkdown); + }); +}); + +describe("thirdPartyMessageAttachments", () => { + it("should return an empty array on initial state", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const initialState = appReducer( + undefined, + applicationChangeState("active") + ); + const attachments = thirdPartyMessageAttachments(initialState, messageId); + expect(attachments).toBeDefined(); + expect(attachments.length).toBe(0); + }); + it("should return an empty array on a third party message with no attachments", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const loadedThirdPartyMessage = appReducer( + undefined, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: {} + } as ThirdPartyMessageWithContent + }) + ); + const attachments = thirdPartyMessageAttachments( + loadedThirdPartyMessage, + messageId + ); + expect(attachments).toBeDefined(); + expect(attachments.length).toBe(0); + }); + it("should return an empty array on a third party message with empty attachments", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const loadedThirdPartyMessage = appReducer( + undefined, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: { + attachments: [] + } as ThirdPartyMessage + } as ThirdPartyMessageWithContent + }) + ); + const attachments = thirdPartyMessageAttachments( + loadedThirdPartyMessage, + messageId + ); + expect(attachments).toBeDefined(); + expect(attachments.length).toBe(0); + }); + it("should return an empty array on a third party message with empty attachments", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url" + } as ThirdPartyAttachment; + const loadedThirdPartyMessage = appReducer( + undefined, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: { + attachments: [thirdPartyAttachment] + } as ThirdPartyMessage + } as ThirdPartyMessageWithContent + }) + ); + const attachments = thirdPartyMessageAttachments( + loadedThirdPartyMessage, + messageId + ); + expect(attachments).toBeDefined(); + expect(attachments.length).toBe(1); + expect(attachments[0]).toMatchObject(thirdPartyAttachment); + }); +}); + +describe("hasAttachmentsSelector", () => { + it("should return false if there are no attachments", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const loadedThirdPartyMessage = appReducer( + undefined, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: {} + } as ThirdPartyMessageWithContent + }) + ); + const hasAttachments = hasAttachmentsSelector( + loadedThirdPartyMessage, + messageId + ); + expect(hasAttachments).toBe(false); + }); + + it("should return true if there are attachments", () => { + const messageId = "01HNWRS7DP721KTC3SMCJ7G82E" as UIMessageId; + const thirdPartyAttachment = { + id: "1", + url: "https://invalid.url" + } as ThirdPartyAttachment; + const loadedThirdPartyMessage = appReducer( + undefined, + loadThirdPartyMessage.success({ + id: messageId, + content: { + third_party_message: { + attachments: [thirdPartyAttachment] + } as ThirdPartyMessage + } as ThirdPartyMessageWithContent + }) + ); + const hasAttachments = hasAttachmentsSelector( + loadedThirdPartyMessage, + messageId + ); + expect(hasAttachments).toBe(true); + }); }); diff --git a/ts/features/messages/store/reducers/__tests__/transformers.test.ts b/ts/features/messages/store/reducers/__tests__/transformers.test.ts index 1cc5873396e..df2802db384 100644 --- a/ts/features/messages/store/reducers/__tests__/transformers.test.ts +++ b/ts/features/messages/store/reducers/__tests__/transformers.test.ts @@ -1,7 +1,8 @@ import { CreatedMessageWithContentAndAttachments } from "../../../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; +import { ThirdPartyAttachment } from "../../../../../../definitions/backend/ThirdPartyAttachment"; import { message_1 } from "../../../__mocks__/message"; -import { toUIMessageDetails } from "../transformers"; +import { attachmentDisplayName, toUIMessageDetails } from "../transformers"; const inputWithoutDueDate: CreatedMessageWithContentAndAttachments = { ...message_1, @@ -18,3 +19,21 @@ describe("`toUIMessageDetails` function", () => { }); }); }); + +describe("attachmentDisplayName", () => { + it("should properly convert name giving a display name source", () => { + const thirdPartyAttachment = { + id: "1", + name: "The name" + } as ThirdPartyAttachment; + const displayName = attachmentDisplayName(thirdPartyAttachment); + expect(displayName).toBe(thirdPartyAttachment.name); + }); + it("should properly convert name giving an unavailable name source", () => { + const thirdPartyAttachment = { + id: "1" + } as ThirdPartyAttachment; + const displayName = attachmentDisplayName(thirdPartyAttachment); + expect(displayName).toBe(thirdPartyAttachment.id); + }); +}); diff --git a/ts/features/messages/store/reducers/detailsById.ts b/ts/features/messages/store/reducers/detailsById.ts index 052c1fc3eba..3f14236acbc 100644 --- a/ts/features/messages/store/reducers/detailsById.ts +++ b/ts/features/messages/store/reducers/detailsById.ts @@ -21,7 +21,7 @@ const INITIAL_STATE: DetailsById = {}; /** * A reducer to store all messages details by ID */ -const reducer = ( +export const detailsByIdReducer = ( state: DetailsById = INITIAL_STATE, action: Action ): DetailsById => { @@ -86,4 +86,28 @@ export const detailedMessageHasThirdPartyDataSelector = ( ) ); -export default reducer; +export const messageDetailsExpiringInfoSelector = ( + state: GlobalState, + id: string, + referenceDateMilliseconds: number +) => + pipe( + messageDetailsByIdSelector(state, id), + pot.toOption, + O.filter(messageDetails => !!messageDetails.paymentData), + O.chainNullableK(messageDetails => messageDetails.dueDate), + O.map(dueDate => { + const remainingMilliseconds = + dueDate.getTime() - referenceDateMilliseconds; + return remainingMilliseconds > 0 ? "expiring" : "expired"; + }), + O.getOrElseW(() => "does_not_expire" as const) + ); + +export const messagePaymentDataSelector = (state: GlobalState, id: string) => + pipe( + messageDetailsByIdSelector(state, id), + pot.toOption, + O.chainNullableK(message => message.paymentData), + O.toUndefined + ); diff --git a/ts/features/messages/store/reducers/downloads.ts b/ts/features/messages/store/reducers/downloads.ts index 3cf6630f74e..e3234a27905 100644 --- a/ts/features/messages/store/reducers/downloads.ts +++ b/ts/features/messages/store/reducers/downloads.ts @@ -1,9 +1,13 @@ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { createSelector } from "reselect"; import { getType } from "typesafe-actions"; -import { downloadAttachment, removeCachedAttachment } from "../actions"; +import { + DownloadAttachmentCancel, + clearRequestedAttachmentDownload, + downloadAttachment, + removeCachedAttachment +} from "../actions"; import { Action } from "../../../../store/actions/types"; import { IndexedById } from "../../../../store/helpers/indexer"; import { @@ -13,22 +17,28 @@ import { toSome } from "../../../../store/reducers/IndexedByIdPot"; import { GlobalState } from "../../../../store/reducers/types"; -import { UIAttachment, UIMessageId, UIAttachmentId } from "../../types"; +import { UIMessageId } from "../../types"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; export type Download = { - attachment: UIAttachment; + attachment: ThirdPartyAttachment; path: string; }; -export type DownloadError = { - attachment: UIAttachment; - error: T; +export type DownloadError = { + attachment: ThirdPartyAttachment; + error: Error; +}; + +type RequestedDownload = { + messageId: UIMessageId; + attachmentId: string; }; export type Downloads = Record< UIMessageId, - IndexedById> ->; + IndexedById> | undefined +> & { requestedDownload?: RequestedDownload }; export const INITIAL_STATE: Downloads = {}; @@ -44,16 +54,20 @@ export const downloadsReducer = ( return { ...state, [action.payload.messageId]: toLoading( - action.payload.id, + action.payload.attachment.id, state[action.payload.messageId] ?? {} - ) + ), + requestedDownload: { + messageId: action.payload.messageId, + attachmentId: action.payload.attachment.id + } }; case getType(downloadAttachment.success): return { ...state, - [action.payload.attachment.messageId]: toSome( + [action.payload.messageId]: toSome( action.payload.attachment.id, - state[action.payload.attachment.messageId] ?? {}, + state[action.payload.messageId] ?? {}, { attachment: action.payload.attachment, path: action.payload.path @@ -63,9 +77,9 @@ export const downloadsReducer = ( case getType(downloadAttachment.failure): return { ...state, - [action.payload.attachment.messageId]: toError( + [action.payload.messageId]: toError( action.payload.attachment.id, - state[action.payload.attachment.messageId] ?? {}, + state[action.payload.messageId] ?? {}, action.payload.error ) }; @@ -74,18 +88,27 @@ export const downloadsReducer = ( return { ...state, [action.payload.messageId]: toNone( - action.payload.id, + action.payload.attachment.id, state[action.payload.messageId] ?? {} + ), + requestedDownload: requestDownloadAfterCancelledAction( + state, + action.payload ) }; case getType(removeCachedAttachment): return { ...state, - [action.payload.attachment.messageId]: toNone( + [action.payload.messageId]: toNone( action.payload.attachment.id, - state[action.payload.attachment.messageId] ?? {} + state[action.payload.messageId] ?? {} ) }; + case getType(clearRequestedAttachmentDownload): + return { + ...state, + requestedDownload: undefined + }; } return state; }; @@ -93,27 +116,56 @@ export const downloadsReducer = ( /** * From attachment to the download pot */ -export const downloadPotForMessageAttachmentSelector = createSelector( - [ - (state: GlobalState) => state.entities.messages.downloads, - ( - _: GlobalState, - attachment: { messageId: UIMessageId; id: UIAttachmentId } - ) => attachment - ], - (downloads, attachment): pot.Pot => { - const download = downloads[attachment.messageId]; - if (download) { - return download[attachment.id] ?? pot.none; - } - return pot.none; - } -); +export const downloadPotForMessageAttachmentSelector = ( + state: GlobalState, + messageId: UIMessageId, + attachmentId: string +) => state.entities.messages.downloads[messageId]?.[attachmentId] ?? pot.none; + +export const isRequestedAttachmentDownloadSelector = ( + state: GlobalState, + messageId: UIMessageId, + attachmentId: string +) => + isRequestedDownloadMatch( + state.entities.messages.downloads.requestedDownload, + messageId, + attachmentId + ); + +export const isDownloadingMessageAttachmentSelector = ( + state: GlobalState, + messageId: UIMessageId, + attachmentId: string +) => + pipe( + state.entities.messages.downloads[messageId], + O.fromNullable, + O.chainNullableK(messageDownloads => messageDownloads[attachmentId]), + O.getOrElseW(() => pot.none), + pot.isLoading + ); + +export const hasErrorOccourredOnRequestedDownloadSelector = ( + state: GlobalState, + messageId: UIMessageId, + attachmentId: string +) => + pipe( + state.entities.messages.downloads[messageId], + O.fromNullable, + O.chainNullableK(messageDownloads => messageDownloads[attachmentId]), + O.filter(() => + isRequestedAttachmentDownloadSelector(state, messageId, attachmentId) + ), + O.getOrElseW(() => pot.none), + downloadPot => pot.isError(downloadPot) && !pot.isSome(downloadPot) + ); export const downloadedMessageAttachmentSelector = ( state: GlobalState, messageId: UIMessageId, - attachmentId: UIAttachmentId + attachmentId: string ) => pipe( state.entities.messages.downloads[messageId], @@ -123,3 +175,23 @@ export const downloadedMessageAttachmentSelector = ( O.flatten, O.toUndefined ); + +const isRequestedDownloadMatch = ( + requestedDownload: RequestedDownload | undefined, + messageId: UIMessageId, + attachmentId: string +) => + !!requestedDownload && + requestedDownload.messageId === messageId && + requestedDownload.attachmentId === attachmentId; +const requestDownloadAfterCancelledAction = ( + state: Downloads, + cancelActionPayload: DownloadAttachmentCancel +) => + isRequestedDownloadMatch( + state.requestedDownload, + cancelActionPayload.messageId, + cancelActionPayload.attachment.id + ) + ? undefined + : state.requestedDownload; diff --git a/ts/features/messages/store/reducers/index.ts b/ts/features/messages/store/reducers/index.ts index 8c31bed74a4..bd535eb4d2d 100644 --- a/ts/features/messages/store/reducers/index.ts +++ b/ts/features/messages/store/reducers/index.ts @@ -7,7 +7,7 @@ import { combineReducers } from "redux"; import { Action } from "../../../../store/actions/types"; import allPaginatedReducer, { AllPaginated } from "./allPaginated"; -import detailsByIdReducer, { DetailsById } from "./detailsById"; +import { DetailsById, detailsByIdReducer } from "./detailsById"; import paginatedByIdReducer, { PaginatedById } from "./paginatedById"; import { thirdPartyByIdReducer, ThirdPartyById } from "./thirdPartyById"; import { Downloads, downloadsReducer } from "./downloads"; @@ -16,6 +16,7 @@ import { messagePreconditionReducer } from "./messagePrecondition"; import { MessageGetStatus, messageGetStatusReducer } from "./messageGetStatus"; +import { MultiplePaymentState, paymentsReducer } from "./payments"; export type MessagesState = Readonly<{ allPaginated: AllPaginated; @@ -25,6 +26,7 @@ export type MessagesState = Readonly<{ downloads: Downloads; messagePrecondition: MessagePrecondition; messageGetStatus: MessageGetStatus; + payments: MultiplePaymentState; }>; const reducer = combineReducers({ @@ -34,7 +36,8 @@ const reducer = combineReducers({ thirdPartyById: thirdPartyByIdReducer, downloads: downloadsReducer, messagePrecondition: messagePreconditionReducer, - messageGetStatus: messageGetStatusReducer + messageGetStatus: messageGetStatusReducer, + payments: paymentsReducer }); export default reducer; diff --git a/ts/features/messages/store/reducers/payments.ts b/ts/features/messages/store/reducers/payments.ts new file mode 100644 index 00000000000..3044139c9e0 --- /dev/null +++ b/ts/features/messages/store/reducers/payments.ts @@ -0,0 +1,233 @@ +import { pipe } from "fp-ts/lib/function"; +import * as B from "fp-ts/lib/boolean"; +import * as O from "fp-ts/lib/Option"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../store/actions/types"; +import { UIMessageDetails, UIMessageId } from "../../types"; +import { GlobalState } from "../../../../store/reducers/types"; +import { + foldK, + foldKW, + isLoading, + isUndefined, + remoteError, + remoteLoading, + remoteReady, + remoteUndefined, + RemoteValue +} from "../../../../common/model/RemoteValue"; +import { + addUserSelectedPaymentRptId, + reloadAllMessages, + updatePaymentForMessage +} from "../actions"; +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; +import { isProfileEmailValidatedSelector } from "../../../../store/reducers/profile"; +import { isPagoPaSupportedSelector } from "../../../../common/versionInfo/store/reducers/versionInfo"; +import { + duplicateSetAndAdd, + duplicateSetAndRemove, + getRptIdStringFromPaymentData +} from "../../utils"; +import { isExpiredPaymentFromDetailV2Enum } from "../../../../utils/payment"; +import { + messageDetailsByIdSelector, + messagePaymentDataSelector +} from "./detailsById"; + +export type MultiplePaymentState = { + [key: UIMessageId]: SinglePaymentState | undefined; + userSelectedPayments: Set; +}; + +export type SinglePaymentState = { + [key: string]: + | RemoteValue + | undefined; +}; + +export const initialState: MultiplePaymentState = { + userSelectedPayments: new Set() +}; + +export const paymentsReducer = ( + state: MultiplePaymentState = initialState, + action: Action +): MultiplePaymentState => { + switch (action.type) { + case getType(updatePaymentForMessage.request): + return { + ...state, + [action.payload.messageId]: { + ...state[action.payload.messageId], + [action.payload.paymentId]: remoteLoading + }, + userSelectedPayments: duplicateSetAndRemove( + state.userSelectedPayments, + action.payload.paymentId + ) + }; + case getType(updatePaymentForMessage.success): + return { + ...state, + [action.payload.messageId]: { + ...state[action.payload.messageId], + [action.payload.paymentId]: remoteReady(action.payload.paymentData) + } + }; + case getType(updatePaymentForMessage.failure): + return { + ...state, + [action.payload.messageId]: { + ...state[action.payload.messageId], + [action.payload.paymentId]: remoteError(action.payload.details) + } + }; + case getType(updatePaymentForMessage.cancel): + return action.payload.reduce( + (previousState, queuedUpdateActionPayload) => ({ + ...previousState, + [queuedUpdateActionPayload.messageId]: { + ...previousState[queuedUpdateActionPayload.messageId], + [queuedUpdateActionPayload.paymentId]: undefined + } + }), + state + ); + case getType(addUserSelectedPaymentRptId): + return { + ...state, + userSelectedPayments: duplicateSetAndAdd( + state.userSelectedPayments, + action.payload.paymentId + ) + }; + case getType(reloadAllMessages.request): + return initialState; + } + return state; +}; + +const paymentStateSelector = ( + state: GlobalState, + messageId: UIMessageId, + paymentId: string +) => + pipe( + state.entities.messages.payments[messageId], + O.fromNullable, + O.chainNullableK(multiplePaymentState => multiplePaymentState[paymentId]), + O.getOrElse>( + () => remoteUndefined + ) + ); + +export const shouldUpdatePaymentSelector = ( + state: GlobalState, + messageId: UIMessageId, + paymentId: string +) => pipe(paymentStateSelector(state, messageId, paymentId), isUndefined); + +export const paymentStatusForUISelector = ( + state: GlobalState, + messageId: UIMessageId, + paymentId: string +): RemoteValue => + pipe(paymentStateSelector(state, messageId, paymentId), remoteValue => + isLoading(remoteValue) ? remoteUndefined : remoteValue + ); + +export const isUserSelectedPaymentSelector = ( + state: GlobalState, + rptId: string +) => state.entities.messages.payments.userSelectedPayments.has(rptId); + +export const userSelectedPaymentRptIdSelector = ( + state: GlobalState, + message: UIMessageDetails | undefined +) => + pipe( + message, + O.fromNullable, + O.chainNullableK(message => message.paymentData), + O.map(getRptIdStringFromPaymentData), + O.filter(rptId => isUserSelectedPaymentSelector(state, rptId)), + O.toUndefined + ); + +export const canNavigateToPaymentFromMessageSelector = (state: GlobalState) => + pipe( + state, + isProfileEmailValidatedSelector, + B.fold( + () => false, + () => pipe(state, isPagoPaSupportedSelector) + ) + ); + +export const paymentsButtonStateSelector = ( + state: GlobalState, + messageId: UIMessageId +) => + pipe( + messagePaymentDataSelector(state, messageId), + O.fromNullable, + O.map(getRptIdStringFromPaymentData), + O.map(paymentId => paymentStateSelector(state, messageId, paymentId)), + O.map(paymentStatus => + pipe( + paymentStatus, + foldK( + () => "loading" as const, + () => "loading" as const, + _ => "enabled" as const, + _ => "hidden" as const + ) + ) + ), + O.getOrElseW(() => "hidden" as const) + ); + +export const isPaymentsButtonVisibleSelector = ( + state: GlobalState, + messageId: UIMessageId +) => + pipe( + paymentsButtonStateSelector(state, messageId), + status => status !== "hidden" + ); + +export const paymentExpirationBannerStateSelector = ( + state: GlobalState, + messageId: UIMessageId +) => + pipe( + messageDetailsByIdSelector(state, messageId), + pot.toOption, + O.filter(message => !!message.dueDate), + O.chainNullableK(message => message.paymentData), + O.map(getRptIdStringFromPaymentData), + O.map(paymentId => paymentStateSelector(state, messageId, paymentId)), + O.map(paymentState => + pipe( + paymentState, + foldKW( + () => "loading" as const, + () => "loading" as const, + _ => "visibleExpiring" as const, + error => + pipe( + error, + isExpiredPaymentFromDetailV2Enum, + B.fold( + () => "hidden" as const, + () => "visibleExpired" as const + ) + ) + ) + ) + ), + O.getOrElseW(() => "hidden" as const) + ); diff --git a/ts/features/messages/store/reducers/thirdPartyById.ts b/ts/features/messages/store/reducers/thirdPartyById.ts index 8eba504dd75..c88aa3d83d4 100644 --- a/ts/features/messages/store/reducers/thirdPartyById.ts +++ b/ts/features/messages/store/reducers/thirdPartyById.ts @@ -3,6 +3,8 @@ import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { getType } from "typesafe-actions"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; +import * as RA from "fp-ts/lib/ReadonlyArray"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; import { loadThirdPartyMessage, reloadAllMessages } from "../actions"; import { Action } from "../../../../store/actions/types"; @@ -14,8 +16,7 @@ import { } from "../../../../store/reducers/IndexedByIdPot"; import { GlobalState } from "../../../../store/reducers/types"; import { RemoteContentDetails } from "../../../../../definitions/backend/RemoteContentDetails"; -import { UIAttachmentId, UIMessageDetails, UIMessageId } from "../../types"; -import { attachmentFromThirdPartyMessage } from "./transformers"; +import { UIMessageDetails, UIMessageId } from "../../types"; export type ThirdPartyById = IndexedById< pot.Pot @@ -84,10 +85,28 @@ export const messageMarkdownSelector = ( messageContent.markdown ); -export const thirdPartyMessageUIAttachment = +export const hasAttachmentsSelector = ( + state: GlobalState, + ioMessageId: UIMessageId +) => pipe(thirdPartyMessageAttachments(state, ioMessageId), RA.isNonEmpty); + +export const thirdPartyMessageAttachments = ( + state: GlobalState, + ioMessageId: UIMessageId +): ReadonlyArray => + pipe( + thirdPartyFromIdSelector(state, ioMessageId), + pot.toOption, + O.chainNullableK( + thirdPartyMessage => thirdPartyMessage.third_party_message.attachments + ), + O.getOrElse>(() => []) + ); + +export const thirdPartyMessageAttachment = (state: GlobalState) => (ioMessageId: UIMessageId) => - (thirdPartyMessageAttachmentId: UIAttachmentId) => + (thirdPartyMessageAttachmentId: string): O.Option => pipe( thirdPartyFromIdSelector(state, ioMessageId), pot.toOption, @@ -100,12 +119,6 @@ export const thirdPartyMessageUIAttachment = thirdPartyMessageAttachment.id === (thirdPartyMessageAttachmentId as string as NonEmptyString) ) - ), - O.map(thirdPartyMessageAttachment => - attachmentFromThirdPartyMessage( - ioMessageId, - thirdPartyMessageAttachment - ) ) ); @@ -132,5 +145,12 @@ const messageContentSelector = ( ) ) ), - O.toUndefined + O.getOrElse(() => + pipe( + state.entities.messages.detailsById[ioMessageId] ?? pot.none, + pot.toOption, + O.map(extractionFunction), + O.toUndefined + ) + ) ); diff --git a/ts/features/messages/store/reducers/transformers.ts b/ts/features/messages/store/reducers/transformers.ts index c6a3aaabaaf..a11141fe2c8 100644 --- a/ts/features/messages/store/reducers/transformers.ts +++ b/ts/features/messages/store/reducers/transformers.ts @@ -1,5 +1,3 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import { CreatedMessageWithContentAndAttachments } from "../../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; import { EnrichedMessage } from "../../../../../definitions/backend/EnrichedMessage"; import { MessageCategory } from "../../../../../definitions/backend/MessageCategory"; @@ -7,14 +5,11 @@ import { TagEnum } from "../../../../../definitions/backend/MessageCategoryBase" import { MessageStatusAttributes } from "../../../../../definitions/backend/MessageStatusAttributes"; import { PublicMessage } from "../../../../../definitions/backend/PublicMessage"; import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; -import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; import { apiUrlPrefix } from "../../../../config"; import { ContentTypeValues } from "../../types/contentType"; import { EUCovidCertificate, PaymentData, - UIAttachment, - UIAttachmentId, UIMessage, UIMessageDetails, UIMessageId @@ -107,43 +102,15 @@ export const toUIMessageDetails = ( }; }; -const generateAttachmentUrl = (messageId: string, attachmentUrl: string) => - `${apiUrlPrefix}/api/v1/third-party-messages/${messageId}/attachments/${attachmentUrl.replace( +export const attachmentDisplayName = (attachment: ThirdPartyAttachment) => + attachment.name ?? attachment.id; +export const attachmentContentType = (attachment: ThirdPartyAttachment) => + attachment.content_type ?? ContentTypeValues.applicationOctetStream; +export const attachmentDownloadUrl = ( + messageId: UIMessageId, + attachment: ThirdPartyAttachment +) => + `${apiUrlPrefix}/api/v1/third-party-messages/${messageId}/attachments/${attachment.url.replace( /^\//g, // note that attachmentUrl might contains a / at the beginning, so let's strip it "" )}`; - -export const attachmentsFromThirdPartyMessage = ( - messageFromApi: ThirdPartyMessageWithContent -): O.Option> => - pipe( - messageFromApi.third_party_message.attachments, - O.fromNullable, - O.map(thirdPartyMessageAttachmentArray => - thirdPartyMessageAttachmentArray.map(thirdPartyMessageAttachment => - attachmentFromThirdPartyMessage( - messageFromApi.id, - thirdPartyMessageAttachment - ) - ) - ) - ); - -export const attachmentFromThirdPartyMessage = ( - thirdPartyMessageId: string, - thirPartyMessageAttachment: ThirdPartyAttachment -): UIAttachment => ({ - messageId: thirdPartyMessageId as UIMessageId, - id: thirPartyMessageAttachment.id as string as UIAttachmentId, - displayName: thirPartyMessageAttachment.name ?? thirPartyMessageAttachment.id, - contentType: - thirPartyMessageAttachment.content_type ?? - ContentTypeValues.applicationOctetStream, - resourceUrl: { - href: generateAttachmentUrl( - thirdPartyMessageId, - thirPartyMessageAttachment.url - ) - }, - category: thirPartyMessageAttachment.category -}); diff --git a/ts/features/messages/types/__tests__/digitalInformationUnit.test.ts b/ts/features/messages/types/__tests__/digitalInformationUnit.test.ts deleted file mode 100644 index 431b437bb66..00000000000 --- a/ts/features/messages/types/__tests__/digitalInformationUnit.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Byte, formatByte } from "../digitalInformationUnit"; - -describe("formatDigitalInformationUnit", () => { - jest.useFakeTimers(); - describe("When formatByte is called", () => { - describe("And the argument is < 1024", () => { - it("Should format using B", () => { - const formatted0Bytes = formatByte(0 as Byte); - expect(formatted0Bytes).toBe("0.00 B"); - const formattedBytes = formatByte(125 as Byte); - expect(formattedBytes).toBe("125.00 B"); - const formatted1024Bytes = formatByte(1023 as Byte); - expect(formatted1024Bytes).toBe("1,023.00 B"); - }); - }); - describe("And the argument is 1024 <= x < 1024*1024", () => { - it("Should format using kB", () => { - const formatted = formatByte(1024 as Byte); - expect(formatted).toBe("1.00 kB"); - const formattedMax = formatByte((1024 * 1024 - 1) as Byte); - expect(formattedMax).toBe("1,024.00 kB"); - }); - }); - describe("And the argument is 1024*1024 <= x < 1024*1024*1024", () => { - it("Should format using MB", () => { - const formatted = formatByte((1024 * 1024) as Byte); - expect(formatted).toBe("1.00 MB"); - const formattedMax = formatByte((1024 * 1024 * 1024 - 1) as Byte); - expect(formattedMax).toBe("1,024.00 MB"); - }); - }); - describe("And the argument is 1024*1024*1024 <= x < 1024*1024*1024*1024", () => { - it("Should format using GB", () => { - const formatted = formatByte((1024 * 1024 * 1024) as Byte); - expect(formatted).toBe("1.00 GB"); - const formattedMax = formatByte( - (1024 * 1024 * 1024 * 1024 - 1) as Byte - ); - expect(formattedMax).toBe("1,024.00 GB"); - }); - }); - describe("And the argument is 1024*1024*1024*1024 <= x", () => { - it("Should format using TB", () => { - const formatted = formatByte((1024 * 1024 * 1024 * 1024) as Byte); - expect(formatted).toBe("1.00 TB"); - const formatted1 = formatByte( - (1024 * 1024 * 1024 * 1024 * 1024 * 1024) as Byte - ); - expect(formatted1).toBe("1,048,576.00 TB"); - - const formatted2 = formatByte( - (1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024 * - 1024) as Byte - ); - expect(formatted2).toBe("1.2089258196146292e+24 TB"); - }); - }); - }); -}); diff --git a/ts/features/messages/types/digitalInformationUnit.ts b/ts/features/messages/types/digitalInformationUnit.ts deleted file mode 100644 index 0cd7b930030..00000000000 --- a/ts/features/messages/types/digitalInformationUnit.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { IUnitTag } from "@pagopa/ts-commons/lib/units"; -import I18n from "../../../i18n"; - -export type Byte = number & IUnitTag<"Byte">; -export type KiloByte = number & IUnitTag<"KiloByte">; -export type MegaByte = number & IUnitTag<"MegaByte">; -export type GigaByte = number & IUnitTag<"GigaByte">; -export type TeraByte = number & IUnitTag<"TeraByte">; - -const unitOrder = ["B", "kB", "MB", "GB", "TB"]; - -/** - * Generate a textual representation for a Digital Information Unit - * @param b - * @param startIndex the starting Digital Information Unit - */ -const internalFormatDigitalInformationUnit = ( - b: number, - startIndex: number -) => { - // eslint-disable-next-line functional/no-let - let acc: number = b; - // eslint-disable-next-line functional/no-let - let index = startIndex; - while (acc >= 1024 && index < unitOrder.length - 1) { - acc /= 1024; - index++; - } - const formatRepresentation = I18n.toNumber(acc, { - precision: 2, - delimiter: I18n.t("global.localization.delimiterSeparator"), - separator: I18n.t("global.localization.decimalSeparator") - }); - - return `${formatRepresentation} ${unitOrder[index]}`; -}; - -export const formatByte = (b: Byte) => - internalFormatDigitalInformationUnit(b, 0); - -export const formatKiloByte = (kB: KiloByte) => - internalFormatDigitalInformationUnit(kB, 1); -export const formatMegaByte = (mB: MegaByte) => - internalFormatDigitalInformationUnit(mB, 2); -export const formatGigaByte = (gB: GigaByte) => - internalFormatDigitalInformationUnit(gB, 3); -export const formatTeraByte = (tB: TeraByte) => - internalFormatDigitalInformationUnit(tB, 4); diff --git a/ts/features/messages/types/index.ts b/ts/features/messages/types/index.ts index 7c8fd6d587a..f90227f2b8a 100644 --- a/ts/features/messages/types/index.ts +++ b/ts/features/messages/types/index.ts @@ -1,5 +1,4 @@ import { IUnitTag } from "@pagopa/ts-commons/lib/units"; -import { ValidUrl } from "@pagopa/ts-commons/lib/url"; import { CreatedMessageWithContentAndAttachments } from "../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; import { FiscalCode } from "../../../../definitions/backend/FiscalCode"; import { MessageBodyMarkdown } from "../../../../definitions/backend/MessageBodyMarkdown"; @@ -10,7 +9,6 @@ import { PublicMessage } from "../../../../definitions/backend/PublicMessage"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { TimeToLiveSeconds } from "../../../../definitions/backend/TimeToLiveSeconds"; import { MessageCategory } from "../../../../definitions/backend/MessageCategory"; -import { Byte } from "./digitalInformationUnit"; /** * The unique ID of a UIMessage and UIMessageDetails, used to avoid passing the wrong ID as parameters @@ -67,35 +65,3 @@ export type PaymentData = { invalidAfterDueDate?: boolean; noticeNumber: PaymentNoticeNumber; }; - -export type Attachment = { - name: string; - content: string; - mimeType: string; -}; - -export type UIAttachmentId = string & IUnitTag<"UIAttachmentId">; - -export type WithSkipMixpanelTrackingOnFailure = T & { - skipMixpanelTrackingOnFailure: boolean; -}; - -/** - * Represent an attachment with the metadata and resourceUrl to retrieve the attachment - */ -export type UIAttachment = { - // the message ID that contains the attachment - messageId: UIMessageId; - // the ID of the attachment (only guaranteed to be unique per message) - id: UIAttachmentId; - // a display name for the file - displayName: string; - // a generic content type for a file - contentType: string; - // size (in Byte) of the attachment, for display purpose - size?: Byte; - // The url that can be used to retrieve the resource - resourceUrl: ValidUrl; - // This category is needed to distinguish between generic and f24 attachments - category?: string; -}; diff --git a/ts/features/messages/utils/__tests__/index.test.ts b/ts/features/messages/utils/__tests__/index.test.ts new file mode 100644 index 00000000000..faf0efdc523 --- /dev/null +++ b/ts/features/messages/utils/__tests__/index.test.ts @@ -0,0 +1,404 @@ +import { Dispatch } from "redux"; +import { + duplicateSetAndAdd, + duplicateSetAndRemove, + getRptIdStringFromPaymentData, + initializeAndNavigateToWalletForPayment +} from ".."; +import { PaymentData, UIMessageId } from "../../types"; +import NavigationService from "../../../../navigation/NavigationService"; +import ROUTES from "../../../../navigation/routes"; +import { addUserSelectedPaymentRptId } from "../../store/actions"; +import { paymentInitializeState } from "../../../../store/actions/wallet/payment"; +import * as pnAnalytics from "../../../pn/analytics/index"; +import { PaymentAmount } from "../../../../../definitions/backend/PaymentAmount"; + +describe("getRptIdStringFromPaymentData", () => { + it("should properly format the RptID", () => { + const fiscalCode = "01234567890"; + const noticeNumber = "012345678912345610"; + const paymentData = { + noticeNumber, + payee: { + fiscalCode + } + } as PaymentData; + const rptId = getRptIdStringFromPaymentData(paymentData); + expect(rptId).toBe(`${fiscalCode}${noticeNumber}`); + }); +}); + +describe("intializeAndNavigateToWalletForPayment", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + it("should call error callback on bad rptId", () => { + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const paymentId = "badRptId"; + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + initializeAndNavigateToWalletForPayment( + "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId, + paymentId, + false, + undefined, + true, + {} as Dispatch, + false, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).toHaveBeenCalledTimes(1); + expect(prenavigationCallback).not.toHaveBeenCalled(); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + it("should call pre navigation callback on good rptId and navigate to wallet home", () => { + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const paymentId = "01234567890012345678912345610"; + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + initializeAndNavigateToWalletForPayment( + "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId, + paymentId, + false, + undefined, + false, + {} as Dispatch, + false, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(prenavigationCallback).toHaveBeenCalledTimes(1); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.MAIN, { + screen: ROUTES.WALLET_HOME, + params: { + newMethodAdded: false + } + }); + }); + it("should navigate to Payment Transaction Summary with default 0-amount", () => { + const analyticsSpy = jest + .spyOn(pnAnalytics, "trackPNPaymentStart") + .mockImplementation(() => undefined); + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const organizationFiscalCode = "11111111111"; + const auxDigit = "0"; + const applicationCode = "22"; + const iuv13 = "3333333333333"; + const checkDigit = "44"; + + const messageId = "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId; + const paymentId = `${organizationFiscalCode}${auxDigit}${applicationCode}${iuv13}${checkDigit}`; + + const dispatch = jest.fn(); + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + false, + undefined, + true, + dispatch, + false, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(prenavigationCallback).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls).toHaveLength(2); + expect(analyticsSpy).not.toHaveBeenCalled(); + expect(dispatch.mock.calls[0][0]).toStrictEqual( + addUserSelectedPaymentRptId(paymentId) + ); + expect(dispatch.mock.calls[1][0]).toStrictEqual(paymentInitializeState()); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: { + organizationFiscalCode, + paymentNoticeNumber: { + applicationCode, + auxDigit, + checkDigit, + iuv13 + } + }, + paymentStartOrigin: "message", + initialAmount: "0000", + messageId + } + }); + }); + it("should navigate to Payment Transaction Summary with default 0-amount and no prenavigation callback", () => { + const analyticsSpy = jest + .spyOn(pnAnalytics, "trackPNPaymentStart") + .mockImplementation(() => undefined); + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const organizationFiscalCode = "11111111111"; + const auxDigit = "0"; + const applicationCode = "22"; + const iuv13 = "3333333333333"; + const checkDigit = "44"; + + const messageId = "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId; + const paymentId = `${organizationFiscalCode}${auxDigit}${applicationCode}${iuv13}${checkDigit}`; + + const dispatch = jest.fn(); + const decodeErrorCallback = jest.fn(); + + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + false, + undefined, + true, + dispatch, + false, + decodeErrorCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(dispatch.mock.calls).toHaveLength(2); + expect(analyticsSpy).not.toHaveBeenCalled(); + expect(dispatch.mock.calls[0][0]).toStrictEqual( + addUserSelectedPaymentRptId(paymentId) + ); + expect(dispatch.mock.calls[1][0]).toStrictEqual(paymentInitializeState()); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: { + organizationFiscalCode, + paymentNoticeNumber: { + applicationCode, + auxDigit, + checkDigit, + iuv13 + } + }, + paymentStartOrigin: "message", + initialAmount: "0000", + messageId + } + }); + }); + it("should navigate to Payment Transaction Summary with given amount and track PN event", () => { + const analyticsSpy = jest + .spyOn(pnAnalytics, "trackPNPaymentStart") + .mockImplementation(() => undefined); + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const organizationFiscalCode = "11111111111"; + const auxDigit = "0"; + const applicationCode = "22"; + const iuv13 = "3333333333333"; + const checkDigit = "44"; + + const messageId = "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId; + const paymentId = `${organizationFiscalCode}${auxDigit}${applicationCode}${iuv13}${checkDigit}`; + const paymentAmount = 199 as PaymentAmount; + + const dispatch = jest.fn(); + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + false, + paymentAmount, + true, + dispatch, + true, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(prenavigationCallback).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls).toHaveLength(2); + expect(analyticsSpy).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls[0][0]).toStrictEqual( + addUserSelectedPaymentRptId(paymentId) + ); + expect(dispatch.mock.calls[1][0]).toStrictEqual(paymentInitializeState()); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: { + organizationFiscalCode, + paymentNoticeNumber: { + applicationCode, + auxDigit, + checkDigit, + iuv13 + } + }, + paymentStartOrigin: "message", + initialAmount: `${paymentAmount}`, + messageId + } + }); + }); + it("should navigate to Payment Transaction Summary with given amount", () => { + const analyticsSpy = jest + .spyOn(pnAnalytics, "trackPNPaymentStart") + .mockImplementation(() => undefined); + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const organizationFiscalCode = "11111111111"; + const auxDigit = "0"; + const applicationCode = "22"; + const iuv13 = "3333333333333"; + const checkDigit = "44"; + + const messageId = "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId; + const paymentId = `${organizationFiscalCode}${auxDigit}${applicationCode}${iuv13}${checkDigit}`; + const paymentAmount = 199 as PaymentAmount; + + const dispatch = jest.fn(); + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + false, + paymentAmount, + true, + dispatch, + false, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(prenavigationCallback).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls).toHaveLength(2); + expect(analyticsSpy).not.toHaveBeenCalled(); + expect(dispatch.mock.calls[0][0]).toStrictEqual( + addUserSelectedPaymentRptId(paymentId) + ); + expect(dispatch.mock.calls[1][0]).toStrictEqual(paymentInitializeState()); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: { + organizationFiscalCode, + paymentNoticeNumber: { + applicationCode, + auxDigit, + checkDigit, + iuv13 + } + }, + paymentStartOrigin: "message", + initialAmount: `${paymentAmount}`, + messageId + } + }); + }); + it("should navigate to Payment Transaction Summary with given amount but not dispatch an `addUserSelectedPaymentRptId`", () => { + const analyticsSpy = jest + .spyOn(pnAnalytics, "trackPNPaymentStart") + .mockImplementation(() => undefined); + const navigateSpy = jest.spyOn(NavigationService, "navigate"); + const organizationFiscalCode = "11111111111"; + const auxDigit = "0"; + const applicationCode = "22"; + const iuv13 = "3333333333333"; + const checkDigit = "44"; + + const messageId = "01HRA60BRYF6BCHF17SMXG8PP2" as UIMessageId; + const paymentId = `${organizationFiscalCode}${auxDigit}${applicationCode}${iuv13}${checkDigit}`; + const paymentAmount = 199 as PaymentAmount; + + const dispatch = jest.fn(); + const decodeErrorCallback = jest.fn(); + const prenavigationCallback = jest.fn(); + + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + true, + paymentAmount, + true, + dispatch, + false, + decodeErrorCallback, + prenavigationCallback + ); + expect(decodeErrorCallback).not.toHaveBeenCalled(); + expect(prenavigationCallback).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls).toHaveLength(1); + expect(analyticsSpy).not.toHaveBeenCalled(); + expect(dispatch.mock.calls[0][0]).toStrictEqual(paymentInitializeState()); + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: { + organizationFiscalCode, + paymentNoticeNumber: { + applicationCode, + auxDigit, + checkDigit, + iuv13 + } + }, + paymentStartOrigin: "message", + initialAmount: `${paymentAmount}`, + messageId + } + }); + }); +}); + +describe("duplicateSetAndAdd", () => { + it("should duplicate input set and add new item", () => { + const inputSet = new Set(); + const newItem = "newItem"; + const outputSet = duplicateSetAndAdd(inputSet, newItem); + expect(inputSet).not.toBe(outputSet); + expect(inputSet.size).toBe(outputSet.size - 1); + expect(inputSet.has(newItem)).toBe(false); + expect(outputSet.has(newItem)).toBe(true); + }); + it("should duplicate input set but not add an existing item", () => { + const inputSet = new Set(); + const existingItem = "existingItem"; + inputSet.add(existingItem); + const duplicatedItem = "existingItem"; + const outputSet = duplicateSetAndAdd(inputSet, duplicatedItem); + expect(inputSet).not.toBe(outputSet); + expect(inputSet.size).toBe(outputSet.size); + expect(inputSet.has(existingItem)).toBe(true); + expect(outputSet.has(existingItem)).toBe(true); + expect(inputSet.has(duplicatedItem)).toBe(true); + expect(outputSet.has(duplicatedItem)).toBe(true); + }); +}); + +describe("duplicateSetAndRemove", () => { + it("should duplicate input set and remove existing item", () => { + const inputSet = new Set(); + const existingItem = "newItem"; + inputSet.add(existingItem); + const outputSet = duplicateSetAndRemove(inputSet, existingItem); + expect(inputSet).not.toBe(outputSet); + expect(inputSet.size).toBe(outputSet.size + 1); + expect(inputSet.has(existingItem)).toBe(true); + expect(outputSet.has(existingItem)).toBe(false); + }); + it("should duplicate input set and do nothing it the item does not exist", () => { + const inputSet = new Set(); + const existingItem = "existingItem"; + inputSet.add(existingItem); + const unmatchingItem = "unmathingItem"; + const outputSet = duplicateSetAndRemove(inputSet, unmatchingItem); + expect(inputSet).not.toBe(outputSet); + expect(inputSet.size).toBe(outputSet.size); + expect(inputSet.has(existingItem)).toBe(true); + expect(outputSet.has(existingItem)).toBe(true); + expect(inputSet.has(unmatchingItem)).toBe(false); + expect(outputSet.has(unmatchingItem)).toBe(false); + }); +}); diff --git a/ts/features/messages/utils/index.ts b/ts/features/messages/utils/index.ts index 5160e625b9e..4e29b4fb26e 100644 --- a/ts/features/messages/utils/index.ts +++ b/ts/features/messages/utils/index.ts @@ -1,9 +1,26 @@ import { pipe } from "fp-ts/lib/function"; -import { NetworkError, getNetworkError } from "../../../utils/errors"; -import { UIMessageDetails } from "../types"; +import * as O from "fp-ts/lib/Option"; +import * as E from "fp-ts/lib/Either"; +import { + AmountInEuroCents, + RptIdFromString +} from "@pagopa/io-pagopa-commons/lib/pagopa"; +import { Dispatch } from "redux"; +import NavigationService from "../../../navigation/NavigationService"; +import ROUTES from "../../../navigation/routes"; +import { paymentInitializeState } from "../../../store/actions/wallet/payment"; import { getExpireStatus } from "../../../utils/dates"; +import { PaymentData, UIMessageDetails, UIMessageId } from "../types"; +import { NetworkError, getNetworkError } from "../../../utils/errors"; +import { PaymentAmount } from "../../../../definitions/backend/PaymentAmount"; +import { getAmountFromPaymentAmount } from "../../../utils/payment"; +import { trackPNPaymentStart } from "../../pn/analytics"; +import { addUserSelectedPaymentRptId } from "../store/actions"; +import { Action } from "../../../store/actions/types"; import { MessagePaymentExpirationInfo } from "./messages"; +export const gapBetweenItemsInAGrid = 8; + const networkErrorToError = (networkError: NetworkError) => networkError.kind === "timeout" ? new Error("timeout") : networkError.value; @@ -28,3 +45,77 @@ export const getPaymentExpirationInfo = ( kind: "UNEXPIRABLE" }; }; + +export const getRptIdStringFromPaymentData = ( + paymentData: PaymentData +): string => `${paymentData.payee.fiscalCode}${paymentData.noticeNumber}`; + +export const initializeAndNavigateToWalletForPayment = ( + messageId: UIMessageId, + paymentId: string, + isPaidOrHasAnError: boolean, + paymentAmount: PaymentAmount | undefined, + canNavigateToPayment: boolean, + dispatch: Dispatch, + isPNPayment: boolean, + decodeErrorCallback: (() => void) | undefined, + preNavigationCallback: (() => void) | undefined = undefined +) => { + const eitherRptId = RptIdFromString.decode(paymentId); + if (E.isLeft(eitherRptId)) { + decodeErrorCallback?.(); + return; + } + + preNavigationCallback?.(); + + if (!canNavigateToPayment) { + // Navigating to Wallet home, having the email address is not validated, + // it will be displayed RemindEmailValidationOverlay + NavigationService.navigate(ROUTES.MAIN, { + screen: ROUTES.WALLET_HOME, + params: { + newMethodAdded: false + } + }); + return; + } + + if (isPNPayment) { + trackPNPaymentStart(); + } + + if (!isPaidOrHasAnError) { + dispatch(addUserSelectedPaymentRptId(paymentId)); + } + dispatch(paymentInitializeState()); + + const initialAmount = pipe( + paymentAmount, + O.fromNullable, + O.map(getAmountFromPaymentAmount), + O.flatten, + O.getOrElse(() => "0000" as AmountInEuroCents) + ); + + NavigationService.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + rptId: eitherRptId.right, + paymentStartOrigin: "message", + initialAmount, + messageId + } + }); +}; + +export const duplicateSetAndAdd = (inputSet: Set, item: T) => { + const outputSet: Set = new Set(inputSet); + return outputSet.add(item); +}; + +export const duplicateSetAndRemove = (inputSet: Set, item: T) => { + const outputSet: Set = new Set(inputSet); + outputSet.delete(item); + return outputSet; +}; diff --git a/ts/features/messages/utils/messages.ts b/ts/features/messages/utils/messages.ts index 4ac4b5902aa..a757bd1abe7 100644 --- a/ts/features/messages/utils/messages.ts +++ b/ts/features/messages/utils/messages.ts @@ -12,7 +12,6 @@ import { CreatedMessageWithContentAndAttachments } from "../../../../definitions import { MessageBodyMarkdown } from "../../../../definitions/backend/MessageBodyMarkdown"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { ServiceMetadata } from "../../../../definitions/backend/ServiceMetadata"; -import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; import { Locales } from "../../../../locales/locales"; import { deriveCustomHandledLink, @@ -24,7 +23,6 @@ import FIMS_ROUTES from "../../fims/navigation/routes"; import { trackMessageCTAFrontMatterDecodingError } from "../analytics"; import { localeFallback } from "../../../i18n"; import NavigationService from "../../../navigation/NavigationService"; -import ROUTES from "../../../navigation/routes"; import { CTA, CTAS, MessageCTA, MessageCTALocales } from "../types/MessageCTA"; import { getExpireStatus } from "../../../utils/dates"; import { @@ -33,6 +31,7 @@ import { } from "../../../utils/internalLink"; import { getLocalePrimaryWithFallback } from "../../../utils/locale"; import { isTextIncludedCaseInsensitive } from "../../../utils/strings"; +import { SERVICES_ROUTES } from "../../services/navigation/routes"; export function messageContainsText( message: CreatedMessageWithContentAndAttachments, @@ -70,18 +69,16 @@ export function messageNeedsCTABar( export const handleCtaAction = ( cta: CTA, linkTo: (path: string) => void, - service?: ServicePublic + serviceId?: ServiceId ) => { if (isIoInternalLink(cta.action)) { const convertedLink = getInternalRoute(cta.action); // the service ID is specifically required for MyPortal webview usage, // not required for other internal screens - if (cta.action.indexOf(ROUTES.SERVICE_WEBVIEW) !== -1) { + if (cta.action.indexOf(SERVICES_ROUTES.SERVICE_WEBVIEW) !== -1) { handleInternalLink( linkTo, - `${convertedLink}${ - service ? "&serviceId=" + (service.service_id as string) : "" - }` + `${convertedLink}${serviceId ? "&serviceId=" + serviceId : ""}` ); return; } @@ -213,7 +210,7 @@ export const getRemoteLocale = (): Extract => E.getOrElseW(() => localeFallback.locale) ); -const extractCTA = ( +const extractCTAs = ( text: string, serviceMetadata?: ServiceMetadata, serviceId?: ServiceId @@ -222,24 +219,20 @@ const extractCTA = ( text, FM.test, O.fromPredicate(identity), - O.chainNullableK(() => { - try { - return FM(text).attributes; - } catch (e) { - trackMessageCTAFrontMatterDecodingError(serviceId); - return null; - } - }), - O.chain(attrs => + O.chain(() => + pipe( + E.tryCatch(() => FM(text).attributes, E.toError), + E.mapLeft(() => trackMessageCTAFrontMatterDecodingError(serviceId)), + O.fromEither + ) + ), + O.chain(attributes => pipe( - attrs[getRemoteLocale()], + attributes[getRemoteLocale()], CTAS.decode, - E.fold( - _ => O.none, - // check if the decoded actions are valid - cta => - hasCtaValidActions(cta, serviceMetadata) ? O.some(cta) : O.none - ) + O.fromEither, + // check if the decoded actions are valid + O.filter(ctas => hasCtaValidActions(ctas, serviceMetadata)) ) ) ); @@ -252,10 +245,10 @@ const extractCTA = ( * @param serviceId */ export const getMessageCTA = ( - markdown: MessageBodyMarkdown, + markdown: MessageBodyMarkdown | string, serviceMetadata?: ServiceMetadata, serviceId?: ServiceId -): O.Option => extractCTA(markdown, serviceMetadata, serviceId); +): O.Option => extractCTAs(markdown, serviceMetadata, serviceId); /** * extract the CTAs from a string given in serviceMetadata such as the front-matter of the message @@ -268,7 +261,7 @@ export const getServiceCTA = ( pipe( serviceMetadata?.cta, O.fromNullable, - O.chain(cta => extractCTA(cta, serviceMetadata)) + O.chain(cta => extractCTAs(cta, serviceMetadata)) ); /** @@ -326,7 +319,9 @@ export const hasCtaValidActions = ( * remove the cta front-matter if it is nested inside the markdown * @param markdown */ -export const cleanMarkdownFromCTAs = (markdown: MessageBodyMarkdown): string => +export const cleanMarkdownFromCTAs = ( + markdown: MessageBodyMarkdown | string +): string => pipe( markdown, FM.test, diff --git a/ts/features/newWallet/components/WalletCardBaseComponent.tsx b/ts/features/newWallet/components/WalletCardBaseComponent.tsx new file mode 100644 index 00000000000..20ee59e95f7 --- /dev/null +++ b/ts/features/newWallet/components/WalletCardBaseComponent.tsx @@ -0,0 +1,40 @@ +import { WithTestID } from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, View } from "react-native"; + +// Wallet card base component props, which declares common props that wallet cards must have +export type WalletCardComponentBaseProps

= WithTestID<{ + isStacked: boolean; + cardProps: P; +}>; + +export const withWalletCardBaseComponent = + < + CardProps extends object, + ContentProps extends WalletCardComponentBaseProps + >( + CardContent: React.ComponentType + ) => + ({ cardProps, isStacked, testID }: ContentProps) => + ( + + + + ); + +export type WalletCardBaseComponent< + CardProps extends object = object, + ContentProps extends WalletCardComponentBaseProps = WalletCardComponentBaseProps +> = ReturnType>; + +const styles = StyleSheet.create({ + container: { + aspectRatio: 16 / 10 + }, + containerStacked: { + aspectRatio: 16 / 3 + } +}); diff --git a/ts/features/newWallet/components/WalletCardSkeleton.tsx b/ts/features/newWallet/components/WalletCardSkeleton.tsx new file mode 100644 index 00000000000..f093e99a09e --- /dev/null +++ b/ts/features/newWallet/components/WalletCardSkeleton.tsx @@ -0,0 +1,60 @@ +import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import Placeholder, { BoxProps } from "rn-placeholder"; + +const WalletCardSkeleton = () => ( + + + + + + + + + + + + + + + + + +); + +const SkeletonPlaceholder = (props: Pick) => ( + +); + +const borderColor = "#0000001F"; + +const styleSheet = StyleSheet.create({ + card: { + aspectRatio: 16 / 10, + backgroundColor: IOColors["grey-100"], + borderRadius: 16, + borderWidth: 1, + borderColor, + paddingTop: 4 + }, + wrapper: { + padding: 16, + flex: 1, + justifyContent: "space-between" + }, + paymentInfo: { + flexDirection: "row", + justifyContent: "space-between" + }, + additionalInfo: { + justifyContent: "space-between" + } +}); + +export { WalletCardSkeleton }; diff --git a/ts/features/newWallet/components/WalletCardsCategoryContainer.tsx b/ts/features/newWallet/components/WalletCardsCategoryContainer.tsx new file mode 100644 index 00000000000..99d4cb5c43e --- /dev/null +++ b/ts/features/newWallet/components/WalletCardsCategoryContainer.tsx @@ -0,0 +1,61 @@ +import { + IOIcons, + ListItemHeader, + VSpacer, + WithTestID +} from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { View } from "react-native"; +import { WalletCard, walletCardComponentMapper } from "../types"; + +export type WalletCategoryStackContainerProps = WithTestID<{ + iconName: IOIcons; + label: string; + cards: ReadonlyArray; +}>; + +/** + * This component handles the rendering of cards of a specific category. + * The component also handles logic behind card stacking and animations + */ +const WalletCardsCategoryContainer = ({ + label, + iconName, + cards, + testID +}: WalletCategoryStackContainerProps) => { + if (cards === undefined || cards.length === 0) { + // If cards are not provided or are an empty array, the component should not render + return null; + } + + const isStacked = cards.length > 1; + + const renderCardFn = (card: WalletCard, stacked: boolean) => { + const Component = walletCardComponentMapper[card.type]; + return ( + Component && ( + + ) + ); + }; + + return ( + + + {cards.map((card, index) => ( + + {!isStacked && index !== 0 && } + {renderCardFn(card, isStacked && index < cards.length - 1)} + + ))} + + + ); +}; + +export { WalletCardsCategoryContainer }; diff --git a/ts/features/newWallet/components/WalletCardsContainer.tsx b/ts/features/newWallet/components/WalletCardsContainer.tsx new file mode 100644 index 00000000000..45b857d30fd --- /dev/null +++ b/ts/features/newWallet/components/WalletCardsContainer.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { View } from "react-native"; +import I18n from "../../../i18n"; +import { useIOSelector } from "../../../store/hooks"; +import { getWalletCardsByCategorySelector } from "../store/selectors"; +import { WalletCardCategory, walletCardCategoryIcons } from "../types"; +import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer"; + +const WalletCardsContainer = () => { + const cardsByCategory = useIOSelector(getWalletCardsByCategorySelector); + + return ( + + {Object.entries(cardsByCategory).map(([categoryString, cards]) => { + const category = categoryString as WalletCardCategory; + + return ( + + ); + })} + + ); +}; + +export { WalletCardsContainer }; diff --git a/ts/features/newWallet/components/WalletEmptyScreenContent.tsx b/ts/features/newWallet/components/WalletEmptyScreenContent.tsx new file mode 100644 index 00000000000..21499182bf8 --- /dev/null +++ b/ts/features/newWallet/components/WalletEmptyScreenContent.tsx @@ -0,0 +1,53 @@ +import { + Body, + ButtonSolid, + IOVisualCostants, + Pictogram, + VSpacer +} from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import I18n from "../../../i18n"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import { WalletRoutes } from "../navigation/routes"; + +const WalletEmptyScreenContent = () => { + const navigation = useIONavigation(); + + const handleAddToWalletButtonPress = () => { + navigation.navigate(WalletRoutes.WALLET_NAVIGATOR, { + screen: WalletRoutes.WALLET_CARD_ONBOARDING + }); + }; + + return ( + + + + + {I18n.t("features.wallet.home.emptyMessage")} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: IOVisualCostants.appMarginDefault, + justifyContent: "center", + alignItems: "center" + }, + text: { textAlign: "center" } +}); + +export { WalletEmptyScreenContent }; diff --git a/ts/features/newWallet/components/WalletPaymentsRedirectBanner.tsx b/ts/features/newWallet/components/WalletPaymentsRedirectBanner.tsx new file mode 100644 index 00000000000..dfb23910e72 --- /dev/null +++ b/ts/features/newWallet/components/WalletPaymentsRedirectBanner.tsx @@ -0,0 +1,53 @@ +import { Banner, VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; +import { View } from "react-native"; +import I18n from "../../../i18n"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import ROUTES from "../../../navigation/routes"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { walletSetPaymentsRedirectBannerVisible } from "../store/actions/preferences"; +import { isWalletPaymentsRedirectBannerVisibleSelector } from "../store/selectors"; + +const WalletPaymentsRedirectBanner = () => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const bannerRef = React.createRef(); + + const isVisible = useIOSelector( + isWalletPaymentsRedirectBannerVisibleSelector + ); + + const handleOnBannerPress = () => { + navigation.navigate(ROUTES.MAIN, { + screen: ROUTES.PAYMENTS_HOME + }); + }; + + const handleOnBannerClose = () => { + dispatch(walletSetPaymentsRedirectBannerVisible(false)); + }; + + if (!isVisible) { + return null; + } + + return ( + <> + + + + ); +}; + +export { WalletPaymentsRedirectBanner }; diff --git a/ts/features/newWallet/components/__tests__/WalletCardBaseComponent.test.tsx b/ts/features/newWallet/components/__tests__/WalletCardBaseComponent.test.tsx new file mode 100644 index 00000000000..c7067df07bf --- /dev/null +++ b/ts/features/newWallet/components/__tests__/WalletCardBaseComponent.test.tsx @@ -0,0 +1,17 @@ +import { H3 } from "@pagopa/io-app-design-system"; +import { render } from "@testing-library/react-native"; +import React from "react"; +import { withWalletCardBaseComponent } from "../WalletCardBaseComponent"; + +describe("WalletCardBaseComponent", () => { + it("should render the card content correctly", () => { + const innerComponent = () =>

Hello!

; + const { queryByText } = render( + withWalletCardBaseComponent(innerComponent)({ + cardProps: {}, + isStacked: false + }) + ); + expect(queryByText("Hello!")).not.toBeNull(); + }); +}); diff --git a/ts/features/newWallet/components/__tests__/WalletCardsCategoryContainer.test.tsx b/ts/features/newWallet/components/__tests__/WalletCardsCategoryContainer.test.tsx new file mode 100644 index 00000000000..06dd20fbb9c --- /dev/null +++ b/ts/features/newWallet/components/__tests__/WalletCardsCategoryContainer.test.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import configureMockStore from "redux-mock-store"; +import ROUTES from "../../../../navigation/routes"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { GlobalState } from "../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { + WalletCardsCategoryContainer, + WalletCategoryStackContainerProps +} from "../WalletCardsCategoryContainer"; + +describe("WalletCardsCategoryContainer", () => { + const T_CATEGORY_LABEL = "Category ABC"; + const T_KEY = "12345"; + it("should correctly render the component", () => { + const { + component: { queryByTestId, queryByText } + } = renderComponent({ + cards: [ + { key: T_KEY, type: "payment", category: "payment", walletId: "" } + ], + iconName: "bonus", + label: T_CATEGORY_LABEL + }); + expect(queryByText(T_CATEGORY_LABEL)).not.toBeNull(); + expect(queryByTestId(`walletCardTestID_${T_KEY}`)).not.toBeNull(); + }); + it("should not render the component if no cards are provided", () => { + const { + component: { queryByTestId, queryByText } + } = renderComponent({ + cards: [], + iconName: "bonus", + label: T_CATEGORY_LABEL + }); + expect(queryByText(T_CATEGORY_LABEL)).toBeNull(); + expect(queryByTestId(`walletCardTestID_${T_KEY}`)).toBeNull(); + }); +}); + +const renderComponent = (props: WalletCategoryStackContainerProps) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(globalState); + + return { + component: renderScreenWithNavigationStoreContext( + () => , + ROUTES.WALLET_HOME, + {}, + store + ), + store + }; +}; diff --git a/ts/features/newWallet/components/__tests__/WalletCardsContainer.test.tsx b/ts/features/newWallet/components/__tests__/WalletCardsContainer.test.tsx new file mode 100644 index 00000000000..2e2a144f215 --- /dev/null +++ b/ts/features/newWallet/components/__tests__/WalletCardsContainer.test.tsx @@ -0,0 +1,106 @@ +import { within } from "@testing-library/react-native"; +import _ from "lodash"; +import * as React from "react"; +import configureMockStore from "redux-mock-store"; +import I18n from "../../../../i18n"; +import ROUTES from "../../../../navigation/routes"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { GlobalState } from "../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { WalletCardsState } from "../../store/reducers/cards"; +import { WalletCardsContainer } from "../WalletCardsContainer"; + +const T_CARDS: WalletCardsState = { + "1": { + key: "1", + type: "payment", + category: "payment", + walletId: "" + }, + "2": { + key: "2", + type: "payment", + category: "payment", + walletId: "" + }, + "3": { + key: "3", + type: "idPay", + category: "bonus", + amount: 1234, + avatarSource: { + uri: "" + }, + expireDate: new Date(), + initiativeId: "", + name: "ABC" + } +}; + +describe("WalletCardsContainer", () => { + it("should render the cards correctly", () => { + const { + component: { queryByText, queryByTestId } + } = renderComponent(); + + expect( + queryByText(I18n.t(`features.wallet.cards.categories.payment`)) + ).not.toBeNull(); + + const paymentCategoryComponent = queryByTestId( + `walletCardsCategoryTestID_payment` + ); + + if (paymentCategoryComponent) { + const { queryByTestId } = within(paymentCategoryComponent); + expect(queryByTestId(`walletCardTestID_1`)).not.toBeNull(); + expect(queryByTestId(`walletCardTestID_2`)).not.toBeNull(); + expect(queryByTestId(`walletCardTestID_3`)).toBeNull(); + } + + expect( + queryByText(I18n.t(`features.wallet.cards.categories.bonus`)) + ).not.toBeNull(); + + const bonusCategoryComponent = queryByTestId( + `walletCardsCategoryTestID_bonus` + ); + + if (bonusCategoryComponent) { + const { queryByTestId } = within(bonusCategoryComponent); + expect(queryByTestId(`walletCardTestID_1`)).toBeNull(); + expect(queryByTestId(`walletCardTestID_2`)).toBeNull(); + expect(queryByTestId(`walletCardTestID_3`)).not.toBeNull(); + } + + expect( + queryByText(I18n.t(`features.wallet.cards.categories.cgn`)) + ).toBeNull(); + }); +}); + +const renderComponent = () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockStore = configureMockStore(); + const store: ReturnType = mockStore( + _.merge(globalState, { + features: { + wallet: { + cards: T_CARDS + } + } + }) + ); + + return { + component: renderScreenWithNavigationStoreContext( + () => , + ROUTES.WALLET_HOME, + {}, + store + ), + store + }; +}; diff --git a/ts/features/newWallet/components/__tests__/WalletEmptyScreenContent.test.tsx b/ts/features/newWallet/components/__tests__/WalletEmptyScreenContent.test.tsx new file mode 100644 index 00000000000..e84410b0d56 --- /dev/null +++ b/ts/features/newWallet/components/__tests__/WalletEmptyScreenContent.test.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import configureMockStore from "redux-mock-store"; +import ROUTES from "../../../../navigation/routes"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { GlobalState } from "../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { WalletEmptyScreenContent } from "../WalletEmptyScreenContent"; + +describe("WalletEmptyScreenContent", () => { + it("should match the snapshot", () => { + const { component } = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(globalState); + + return { + component: renderScreenWithNavigationStoreContext( + () => , + ROUTES.WALLET_HOME, + {}, + store + ), + store + }; +}; diff --git a/ts/features/newWallet/components/__tests__/__snapshots__/WalletEmptyScreenContent.test.tsx.snap b/ts/features/newWallet/components/__tests__/__snapshots__/WalletEmptyScreenContent.test.tsx.snap new file mode 100644 index 00000000000..e29f2cf4500 --- /dev/null +++ b/ts/features/newWallet/components/__tests__/__snapshots__/WalletEmptyScreenContent.test.tsx.snap @@ -0,0 +1,739 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WalletEmptyScreenContent should match the snapshot 1`] = ` + + + + + + + + + + + + + + + WALLET_HOME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custodisci qui i tuoi metodi di pagamento, Carta Giovani Nazionale, bonus e sconti. + + + + + + + + + + + + + Aggiungi al Portafoglio + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/newWallet/navigation/index.tsx b/ts/features/newWallet/navigation/index.tsx new file mode 100644 index 00000000000..3cff2cc2992 --- /dev/null +++ b/ts/features/newWallet/navigation/index.tsx @@ -0,0 +1,20 @@ +import { createStackNavigator } from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../utils/navigation"; +import { WalletCardOnboardingScreen } from "../screens/WalletCardOnboardingScreen"; +import { WalletRoutes } from "./routes"; +import { WalletParamsList } from "./params"; + +const Stack = createStackNavigator(); + +export const WalletNavigator = () => ( + + + +); diff --git a/ts/features/newWallet/navigation/params.ts b/ts/features/newWallet/navigation/params.ts new file mode 100644 index 00000000000..21663dab293 --- /dev/null +++ b/ts/features/newWallet/navigation/params.ts @@ -0,0 +1,5 @@ +import { WalletRoutes } from "./routes"; + +export type WalletParamsList = { + [WalletRoutes.WALLET_CARD_ONBOARDING]: undefined; +}; diff --git a/ts/features/newWallet/navigation/routes.ts b/ts/features/newWallet/navigation/routes.ts new file mode 100644 index 00000000000..e44018bc7a6 --- /dev/null +++ b/ts/features/newWallet/navigation/routes.ts @@ -0,0 +1,6 @@ +// TODO remove `NEW_` prefix after legacy wallet removal +// The prefix is to make the route name unique across all navigation routes +export const WalletRoutes = { + WALLET_NAVIGATOR: "NEW_WALLET_NAVIGATOR", + WALLET_CARD_ONBOARDING: "NEW_WALLET_CARD_ONBOARDING" +} as const; diff --git a/ts/features/newWallet/screens/WalletCardOnboardingScreen.tsx b/ts/features/newWallet/screens/WalletCardOnboardingScreen.tsx new file mode 100644 index 00000000000..0b9c3c2bc4a --- /dev/null +++ b/ts/features/newWallet/screens/WalletCardOnboardingScreen.tsx @@ -0,0 +1,83 @@ +import { + ContentWrapper, + ModuleCredential, + VSpacer +} from "@pagopa/io-app-design-system"; +import React from "react"; +import cgnLogo from "../../../../img/bonus/cgn/cgn_logo.png"; +import { RNavScreenWithLargeHeader } from "../../../components/ui/RNavScreenWithLargeHeader"; +import I18n from "../../../i18n"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { isIdPayEnabledSelector } from "../../../store/reducers/backendStatus"; +import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; +import { cgnActivationStart } from "../../bonus/cgn/store/actions/activation"; +import { isCgnInformationAvailableSelector } from "../../bonus/cgn/store/reducers/details"; +import { PaymentsOnboardingRoutes } from "../../payments/onboarding/navigation/routes"; +import { loadAvailableBonuses } from "../../bonus/common/store/actions/availableBonusesTypes"; + +const WalletCardOnboardingScreen = () => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + + const isIdPayEnabled = useIOSelector(isIdPayEnabledSelector); + const isCgnAlreadyActive = useIOSelector(isCgnInformationAvailableSelector); + + const startCgnActiviation = () => { + dispatch(loadAvailableBonuses.request()); + dispatch(cgnActivationStart()); + }; + + const navigateToInitiativesList = () => { + // TODO add navigation to welfare initiatives list + }; + + const navigateToPaymentMethodOnboarding = () => { + navigation.navigate(PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR, { + screen: PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD + }); + }; + + return ( + + + + + + {isIdPayEnabled && ( + <> + + + + )} + + + + ); +}; + +export { WalletCardOnboardingScreen }; diff --git a/ts/features/newWallet/screens/WalletHomeScreen.tsx b/ts/features/newWallet/screens/WalletHomeScreen.tsx new file mode 100644 index 00000000000..240874b761b --- /dev/null +++ b/ts/features/newWallet/screens/WalletHomeScreen.tsx @@ -0,0 +1,93 @@ +import { + ContentWrapper, + GradientScrollView, + IOStyles, + IOToast +} from "@pagopa/io-app-design-system"; +import { useFocusEffect } from "@react-navigation/native"; +import React from "react"; +import { ScrollView } from "react-native"; +import Animated, { Layout } from "react-native-reanimated"; +import I18n from "../../../i18n"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../../navigation/params/AppParamsList"; +import { MainTabParamsList } from "../../../navigation/params/MainTabParamsList"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { cgnDetails } from "../../bonus/cgn/store/actions/details"; +import { idPayWalletGet } from "../../idpay/wallet/store/actions"; +import { getPaymentsWalletUserMethods } from "../../payments/wallet/store/actions"; +import { WalletCardsContainer } from "../components/WalletCardsContainer"; +import { WalletEmptyScreenContent } from "../components/WalletEmptyScreenContent"; +import { WalletPaymentsRedirectBanner } from "../components/WalletPaymentsRedirectBanner"; +import { WalletRoutes } from "../navigation/routes"; +import { selectWalletCards } from "../store/selectors"; + +type Props = IOStackNavigationRouteProps; + +const WalletHomeScreen = ({ route }: Props) => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + + const cards = useIOSelector(selectWalletCards); + const isNewElementAdded = React.useRef(route.params?.newMethodAdded || false); + + const handleAddToWalletButtonPress = () => { + navigation.navigate(WalletRoutes.WALLET_NAVIGATOR, { + screen: WalletRoutes.WALLET_CARD_ONBOARDING + }); + }; + + React.useEffect(() => { + // TODO SIW-960 Move cards request to app startup + dispatch(getPaymentsWalletUserMethods.request()); + dispatch(idPayWalletGet.request()); + dispatch(cgnDetails.request()); + }, [dispatch]); + + // Handles the "New element added" toast display once the user returns to this screen + useFocusEffect( + React.useCallback(() => { + if (isNewElementAdded.current) { + IOToast.success(I18n.t("features.wallet.home.toast.newMethod")); + // eslint-disable-next-line functional/immutable-data + isNewElementAdded.current = false; + } + }, [isNewElementAdded]) + ); + + if (cards.length === 0) { + return ( + + + + + + + + + ); + } + + return ( + + + + + + + ); +}; + +export { WalletHomeScreen }; diff --git a/ts/features/newWallet/screens/__tests__/WalletHomeScreen.test.tsx b/ts/features/newWallet/screens/__tests__/WalletHomeScreen.test.tsx new file mode 100644 index 00000000000..4d6022fae37 --- /dev/null +++ b/ts/features/newWallet/screens/__tests__/WalletHomeScreen.test.tsx @@ -0,0 +1,125 @@ +import _ from "lodash"; +import configureMockStore from "redux-mock-store"; +import ROUTES from "../../../../navigation/routes"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { GlobalState } from "../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { WalletCardsState } from "../../store/reducers/cards"; +import { WalletHomeScreen } from "../WalletHomeScreen"; + +jest.mock("react-native-reanimated", () => ({ + ...require("react-native-reanimated/mock"), + Layout: { + duration: jest.fn() + } +})); + +const T_CARDS: WalletCardsState = { + "1": { + key: "1", + type: "payment", + category: "payment", + walletId: "" + }, + "2": { + key: "2", + type: "payment", + category: "payment", + walletId: "" + }, + "3": { + key: "3", + type: "idPay", + category: "bonus", + amount: 1234, + avatarSource: { + uri: "" + }, + expireDate: new Date(), + initiativeId: "", + name: "ABC" + } +}; + +describe("WalletHomeScreen", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.runAllTimers(); + }); + + it("should correctly render empty screen with redirect banner", () => { + const { + component: { queryByTestId } + } = renderComponent({}, true); + + expect(queryByTestId("walletPaymentsRedirectBannerTestID")).not.toBeNull(); + expect(queryByTestId("walletEmptyScreenContentTestID")).not.toBeNull(); + expect(queryByTestId("walletCardsContainerTestID")).toBeNull(); + expect(queryByTestId("walletAddCardButtonTestID")).toBeNull(); + }); + + it("should correctly render empty screen without redirect banner", () => { + const { + component: { queryByTestId } + } = renderComponent({}, false); + + expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull(); + expect(queryByTestId("walletEmptyScreenContentTestID")).not.toBeNull(); + expect(queryByTestId("walletCardsContainerTestID")).toBeNull(); + expect(queryByTestId("walletAddCardButtonTestID")).toBeNull(); + }); + + it("should correctly render card list screen with redirect banner", () => { + const { + component: { queryByTestId } + } = renderComponent(T_CARDS, true); + + expect(queryByTestId("walletPaymentsRedirectBannerTestID")).not.toBeNull(); + expect(queryByTestId("walletEmptyScreenContentTestID")).toBeNull(); + expect(queryByTestId("walletCardsContainerTestID")).not.toBeNull(); + expect(queryByTestId("walletAddCardButtonTestID")).not.toBeNull(); + }); + + it("should correctly render card list screen without redirect banner", () => { + const { + component: { queryByTestId } + } = renderComponent(T_CARDS, false); + + expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull(); + expect(queryByTestId("walletEmptyScreenContentTestID")).toBeNull(); + expect(queryByTestId("walletCardsContainerTestID")).not.toBeNull(); + expect(queryByTestId("walletAddCardButtonTestID")).not.toBeNull(); + }); +}); + +const renderComponent = ( + cards: WalletCardsState, + shouldShowPaymentsRedirectBanner: boolean +) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockStore = configureMockStore(); + const store: ReturnType = mockStore( + _.merge(globalState, { + features: { + wallet: { + cards, + preferences: { + shouldShowPaymentsRedirectBanner + } + } + } + }) + ); + + return { + component: renderScreenWithNavigationStoreContext( + WalletHomeScreen, + ROUTES.WALLET_HOME, + {}, + store + ), + store + }; +}; diff --git a/ts/features/newWallet/store/__tests__/cards.test.ts b/ts/features/newWallet/store/__tests__/cards.test.ts new file mode 100644 index 00000000000..6f0e2bbf3d4 --- /dev/null +++ b/ts/features/newWallet/store/__tests__/cards.test.ts @@ -0,0 +1,181 @@ +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { WalletCard } from "../../types"; +import { + walletAddCards, + walletRemoveCards, + walletUpsertCard +} from "../actions/cards"; +import { + getWalletCardsCategorySelector, + selectWalletCards +} from "../selectors"; + +const T_CARD_1: WalletCard = { + category: "bonus", + key: "1234", + type: "idPay", + amount: 123, + avatarSource: { + uri: "" + }, + expireDate: new Date(), + initiativeId: "123", + name: "Test" +}; +const T_CARD_2: WalletCard = { + category: "payment", + key: "9999", + type: "payment", + walletId: "" +}; +const T_CARD_3: WalletCard = { + category: "payment", + key: "4444", + type: "payment", + walletId: "" +}; + +describe("Wallet store", () => { + describe("reducer", () => { + it("should start with initial state", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + }); + + it("should add cards to the store", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1, T_CARD_2])); + + expect(store.getState().features.wallet.cards).toEqual({ + [T_CARD_1.key]: T_CARD_1, + [T_CARD_2.key]: T_CARD_2 + }); + }); + + it("should update a specific card in the store", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1 + }); + + store.dispatch(walletUpsertCard({ ...T_CARD_1, type: "cgn" })); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: { ...T_CARD_1, type: "cgn" } + }); + }); + + it("should add a card in the store if not present another with the same key", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1 + }); + + store.dispatch(walletUpsertCard(T_CARD_2)); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1, + [T_CARD_2.key]: T_CARD_2 + }); + }); + + it("should remove cards from the store", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1, T_CARD_2, T_CARD_3])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1, + [T_CARD_2.key]: T_CARD_2, + [T_CARD_3.key]: T_CARD_3 + }); + + store.dispatch(walletRemoveCards([T_CARD_1.key, T_CARD_3.key])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_2.key]: T_CARD_2 + }); + }); + }); + describe("selectors", () => { + it("should get all the cards from the wallet", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1, T_CARD_2, T_CARD_3])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1, + [T_CARD_2.key]: T_CARD_2, + [T_CARD_3.key]: T_CARD_3 + }); + + expect(selectWalletCards(store.getState())).toEqual( + expect.arrayContaining([T_CARD_1, T_CARD_2, T_CARD_3]) + ); + }); + + it("should get all wallet cards for a specific category", () => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + expect(globalState.features.wallet.cards).toStrictEqual({}); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletAddCards([T_CARD_1, T_CARD_2, T_CARD_3])); + + expect(store.getState().features.wallet.cards).toStrictEqual({ + [T_CARD_1.key]: T_CARD_1, + [T_CARD_2.key]: T_CARD_2, + [T_CARD_3.key]: T_CARD_3 + }); + + expect( + getWalletCardsCategorySelector("payment")(store.getState()) + ).toStrictEqual(expect.arrayContaining([T_CARD_2, T_CARD_3])); + }); + }); +}); diff --git a/ts/features/newWallet/store/actions/cards.ts b/ts/features/newWallet/store/actions/cards.ts new file mode 100644 index 00000000000..cf35a89b1e7 --- /dev/null +++ b/ts/features/newWallet/store/actions/cards.ts @@ -0,0 +1,17 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; +import { WalletCard } from "../../types"; + +export const walletUpsertCard = + createStandardAction("WALLET_UPSERT_CARD")(); + +export const walletAddCards = + createStandardAction("WALLET_ADD_CARDS")>(); + +export const walletRemoveCards = createStandardAction("WALLET_REMOVE_CARDS")< + ReadonlyArray +>(); + +export type WalletCardsActions = + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/newWallet/store/actions/index.ts b/ts/features/newWallet/store/actions/index.ts new file mode 100644 index 00000000000..59b3e917924 --- /dev/null +++ b/ts/features/newWallet/store/actions/index.ts @@ -0,0 +1,4 @@ +import { WalletCardsActions } from "./cards"; +import { WalletPreferencesActions } from "./preferences"; + +export type WalletActions = WalletCardsActions | WalletPreferencesActions; diff --git a/ts/features/newWallet/store/actions/preferences.ts b/ts/features/newWallet/store/actions/preferences.ts new file mode 100644 index 00000000000..fa138ba95c3 --- /dev/null +++ b/ts/features/newWallet/store/actions/preferences.ts @@ -0,0 +1,9 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; + +export const walletSetPaymentsRedirectBannerVisible = createStandardAction( + "WALLET_SET_PAYMENTS_REDIRECT_BANNER_VISIBLE" +)(); + +export type WalletPreferencesActions = ActionType< + typeof walletSetPaymentsRedirectBannerVisible +>; diff --git a/ts/features/newWallet/store/reducers/cards.ts b/ts/features/newWallet/store/reducers/cards.ts new file mode 100644 index 00000000000..6e7d7da2478 --- /dev/null +++ b/ts/features/newWallet/store/reducers/cards.ts @@ -0,0 +1,44 @@ +import { getType } from "typesafe-actions"; +import { Action } from "../../../../store/actions/types"; +import { WalletCard } from "../../types"; +import { + walletAddCards, + walletRemoveCards, + walletUpsertCard +} from "../actions/cards"; + +export type WalletCardsState = { [key: string]: WalletCard }; + +const INITIAL_STATE: WalletCardsState = {}; + +const reducer = ( + state: WalletCardsState = INITIAL_STATE, + action: Action +): WalletCardsState => { + switch (action.type) { + case getType(walletUpsertCard): + return { + ...state, + [action.payload.key]: action.payload + }; + + case getType(walletAddCards): { + return action.payload.reduce( + (obj, card) => ({ + ...obj, + [card.key]: card + }), + state + ); + } + + case getType(walletRemoveCards): { + return Object.fromEntries( + Object.entries(state).filter(([key]) => !action.payload.includes(key)) + ); + } + } + return state; +}; + +export default reducer; diff --git a/ts/features/newWallet/store/reducers/index.ts b/ts/features/newWallet/store/reducers/index.ts new file mode 100644 index 00000000000..b54430b23fe --- /dev/null +++ b/ts/features/newWallet/store/reducers/index.ts @@ -0,0 +1,16 @@ +import { combineReducers } from "redux"; +import { PersistPartial } from "redux-persist"; +import preferencesPersistor, { WalletPreferencesState } from "./preferences"; +import cardsReducer, { WalletCardsState } from "./cards"; + +export type WalletState = { + cards: WalletCardsState; + preferences: WalletPreferencesState & PersistPartial; +}; + +const walletReducer = combineReducers({ + cards: cardsReducer, + preferences: preferencesPersistor +}); + +export default walletReducer; diff --git a/ts/features/newWallet/store/reducers/preferences.ts b/ts/features/newWallet/store/reducers/preferences.ts new file mode 100644 index 00000000000..ab12cdf5f49 --- /dev/null +++ b/ts/features/newWallet/store/reducers/preferences.ts @@ -0,0 +1,43 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { PersistConfig, persistReducer } from "redux-persist"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../store/actions/types"; +import { walletSetPaymentsRedirectBannerVisible } from "../actions/preferences"; + +export type WalletPreferencesState = { + shouldShowPaymentsRedirectBanner: boolean; +}; + +const INITIAL_STATE: WalletPreferencesState = { + shouldShowPaymentsRedirectBanner: true +}; + +const reducer = ( + state: WalletPreferencesState = INITIAL_STATE, + action: Action +): WalletPreferencesState => { + switch (action.type) { + case getType(walletSetPaymentsRedirectBannerVisible): + return { + ...state, + shouldShowPaymentsRedirectBanner: action.payload + }; + } + return state; +}; + +const CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION = -1; + +const persistConfig: PersistConfig = { + key: "walletPreferences", + storage: AsyncStorage, + version: CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION, + whitelist: ["shouldShowPaymentsRedirectBanner"] +}; + +export const walletReducerPersistor = persistReducer< + WalletPreferencesState, + Action +>(persistConfig, reducer); + +export default walletReducerPersistor; diff --git a/ts/features/newWallet/store/selectors/index.ts b/ts/features/newWallet/store/selectors/index.ts new file mode 100644 index 00000000000..45fdcf2c0d1 --- /dev/null +++ b/ts/features/newWallet/store/selectors/index.ts @@ -0,0 +1,32 @@ +import { createSelector } from "reselect"; +import { GlobalState } from "../../../../store/reducers/types"; +import { WalletCard, WalletCardCategory } from "../../types"; + +const selectWalletFeature = (state: GlobalState) => state.features.wallet; + +export const isWalletPaymentsRedirectBannerVisibleSelector = createSelector( + selectWalletFeature, + wallet => wallet.preferences.shouldShowPaymentsRedirectBanner +); + +export const selectWalletCards = createSelector(selectWalletFeature, wallet => + Object.values(wallet.cards) +); + +export const getWalletCardsByCategorySelector = createSelector( + selectWalletCards, + cards => + cards.reduce( + (acc, card) => ({ + ...acc, + [card.category]: [...(acc[card.category] || []), card] + }), + {} as { [category in WalletCardCategory]: ReadonlyArray } + ) +); + +export const getWalletCardsCategorySelector = (category: WalletCardCategory) => + createSelector( + getWalletCardsByCategorySelector, + cardsByCategory => cardsByCategory[category] + ); diff --git a/ts/features/newWallet/types/index.ts b/ts/features/newWallet/types/index.ts new file mode 100644 index 00000000000..eb90436738d --- /dev/null +++ b/ts/features/newWallet/types/index.ts @@ -0,0 +1,71 @@ +import { IOIcons } from "@pagopa/io-app-design-system"; +import { + IdPayWalletCard, + IdPayWalletCardProps +} from "../../idpay/wallet/components/IdPayWalletCard"; +import { + PaymentWalletCard, + PaymentWalletCardProps +} from "../../payments/wallet/components/PaymentWalletCard"; +import { WalletCardBaseComponent } from "../components/WalletCardBaseComponent"; +import { + CgnWalletCard, + CgnWalletCardProps +} from "../../bonus/cgn/components/CgnWalletCard"; + +// Used to group the cards in the wallet. +export type WalletCardCategory = "itw" | "cgn" | "bonus" | "payment"; + +// Basic type definition for a wallet card, describes the properties that +// each card MUST have in order to be placed inside the wallet. +type WalletCardBase = { + key: string; + category: WalletCardCategory; +}; + +// Specific type for ID Pay bonus cards +export type WalletCardBonus = { + type: "idPay"; +} & IdPayWalletCardProps; + +// Specific type for CGN bonus cards +export type WalletCardCgn = { + type: "cgn"; +} & CgnWalletCardProps; + +// Specific type for payment cards +export type WalletCardPayment = { + type: "payment"; +} & PaymentWalletCardProps; + +// Base WalletCard type, which includes all card types +export type WalletCard = WalletCardBase & + (WalletCardBonus | WalletCardCgn | WalletCardPayment); + +// Used to map the card to the specific component that will render the card. +export type WalletCardType = WalletCard["type"]; + +/** + * Wallet card component mapper which translates a WalletCardType to a + * component to be rendered inside the wallet. + * Component MUST be a WalletCardBaseComponent, which can be created + * using {@see withWalletCardBaseComponent} HOC + */ +export const walletCardComponentMapper: Record< + WalletCardType, + WalletCardBaseComponent | undefined +> = { + cgn: CgnWalletCard, + idPay: IdPayWalletCard, + payment: PaymentWalletCard +}; + +/** + * Icons used for each wallet card category + */ +export const walletCardCategoryIcons: Record = { + bonus: "bonus", + payment: "creditCard", + itw: "fiscalCodeIndividual", + cgn: "bonus" +}; diff --git a/ts/features/walletV3/barcode/components/PaymentNoticeListItem.tsx b/ts/features/payments/barcode/components/PaymentNoticeListItem.tsx similarity index 100% rename from ts/features/walletV3/barcode/components/PaymentNoticeListItem.tsx rename to ts/features/payments/barcode/components/PaymentNoticeListItem.tsx diff --git a/ts/features/payments/barcode/navigation/navigator.tsx b/ts/features/payments/barcode/navigation/navigator.tsx new file mode 100644 index 00000000000..c797d6e0556 --- /dev/null +++ b/ts/features/payments/barcode/navigation/navigator.tsx @@ -0,0 +1,47 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp, + TransitionPresets +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import { PaymentsBarcodeChoiceScreen } from "../screens/PaymentsBarcodeChoiceScreen"; +import { PaymentsBarcodeScanScreen } from "../screens/PaymentsBarcodeScanScreen"; +import { PaymentsBarcodeParamsList } from "./params"; +import { PaymentsBarcodeRoutes } from "./routes"; + +const Stack = createStackNavigator(); + +export const WalletBarcodeNavigator = () => ( + + + + +); + +export type PaymentsBarcodeStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type PaymentsBarcodeStackNavigation = PaymentsBarcodeStackNavigationProp< + PaymentsBarcodeParamsList, + keyof PaymentsBarcodeParamsList +>; diff --git a/ts/features/payments/barcode/navigation/params.ts b/ts/features/payments/barcode/navigation/params.ts new file mode 100644 index 00000000000..661b5629a3e --- /dev/null +++ b/ts/features/payments/barcode/navigation/params.ts @@ -0,0 +1,8 @@ +import { PaymentsBarcodeChoiceScreenParams } from "../screens/PaymentsBarcodeChoiceScreen"; +import { PaymentsBarcodeRoutes } from "./routes"; + +export type PaymentsBarcodeParamsList = { + [PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR]: undefined; + [PaymentsBarcodeRoutes.PAYMENT_BARCODE_SCAN]: undefined; + [PaymentsBarcodeRoutes.PAYMENT_BARCODE_CHOICE]: PaymentsBarcodeChoiceScreenParams; +}; diff --git a/ts/features/payments/barcode/navigation/routes.ts b/ts/features/payments/barcode/navigation/routes.ts new file mode 100644 index 00000000000..7a415926cdd --- /dev/null +++ b/ts/features/payments/barcode/navigation/routes.ts @@ -0,0 +1,5 @@ +export const PaymentsBarcodeRoutes = { + PAYMENT_BARCODE_NAVIGATOR: "PAYMENT_BARCODE_NAVIGATOR", + PAYMENT_BARCODE_SCAN: "PAYMENT_BARCODE_SCAN", + PAYMENT_BARCODE_CHOICE: "PAYMENT_BARCODE_CHOICE" +} as const; diff --git a/ts/features/walletV3/barcode/screens/WalletBarcodeChoiceScreen.tsx b/ts/features/payments/barcode/screens/PaymentsBarcodeChoiceScreen.tsx similarity index 76% rename from ts/features/walletV3/barcode/screens/WalletBarcodeChoiceScreen.tsx rename to ts/features/payments/barcode/screens/PaymentsBarcodeChoiceScreen.tsx index 46576c8b5ea..06fbd849435 100644 --- a/ts/features/walletV3/barcode/screens/WalletBarcodeChoiceScreen.tsx +++ b/ts/features/payments/barcode/screens/PaymentsBarcodeChoiceScreen.tsx @@ -31,10 +31,11 @@ import { import { useIODispatch } from "../../../../store/hooks"; import * as analytics from "../../../barcode/analytics"; import { PagoPaBarcode } from "../../../barcode/types/IOBarcode"; +import { usePagoPaPayment } from "../../checkout/hooks/usePagoPaPayment"; import { PaymentNoticeListItem } from "../components/PaymentNoticeListItem"; -import { WalletBarcodeParamsList } from "../navigation/params"; +import { PaymentsBarcodeParamsList } from "../navigation/params"; -type WalletBarcodeChoiceScreenParams = { +type PaymentsBarcodeChoiceScreenParams = { barcodes: Array; }; @@ -43,10 +44,13 @@ const sortByAmount = pipe( contramap((p: PagoPaBarcode) => parseFloat(p.amount)) ); -const WalletBarcodeChoiceScreen = () => { +const PaymentsBarcodeChoiceScreen = () => { const dispatch = useIODispatch(); const navigation = useNavigation>(); + const { startPaymentFlowWithRptId, isNewWalletSectionEnabled } = + usePagoPaPayment(); + useFocusEffect( React.useCallback(() => { analytics.trackBarcodeMultipleCodesScreenView(); @@ -54,7 +58,7 @@ const WalletBarcodeChoiceScreen = () => { ); const route = - useRoute>(); + useRoute>(); const { barcodes } = route.params; @@ -65,15 +69,19 @@ const WalletBarcodeChoiceScreen = () => { : "qrcode_scan"; analytics.trackBarcodeMultipleCodesSelection(); - dispatch(paymentInitializeState()); - navigation.navigate(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, - params: { - initialAmount: barcode.amount, - rptId: barcode.rptId, - paymentStartOrigin - } - }); + if (isNewWalletSectionEnabled) { + startPaymentFlowWithRptId(barcode.rptId); + } else { + dispatch(paymentInitializeState()); + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + initialAmount: barcode.amount, + rptId: barcode.rptId, + paymentStartOrigin + } + }); + } }; const renderBarcodeItem = (barcode: PagoPaBarcode) => { @@ -111,5 +119,5 @@ const WalletBarcodeChoiceScreen = () => { ); }; -export { WalletBarcodeChoiceScreen }; -export type { WalletBarcodeChoiceScreenParams }; +export { PaymentsBarcodeChoiceScreen }; +export type { PaymentsBarcodeChoiceScreenParams }; diff --git a/ts/features/walletV3/barcode/screens/WalletBarcodeScanScreen.tsx b/ts/features/payments/barcode/screens/PaymentsBarcodeScanScreen.tsx similarity index 71% rename from ts/features/walletV3/barcode/screens/WalletBarcodeScanScreen.tsx rename to ts/features/payments/barcode/screens/PaymentsBarcodeScanScreen.tsx index 1eede4ddfe2..0b5d79d64da 100644 --- a/ts/features/walletV3/barcode/screens/WalletBarcodeScanScreen.tsx +++ b/ts/features/payments/barcode/screens/PaymentsBarcodeScanScreen.tsx @@ -1,3 +1,4 @@ +import { IOToast } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; import * as A from "fp-ts/lib/Array"; import { pipe } from "fp-ts/lib/function"; @@ -5,7 +6,6 @@ import * as React from "react"; import ReactNativeHapticFeedback, { HapticFeedbackTypes } from "react-native-haptic-feedback"; -import { IOToast } from "../../../../components/Toast"; import { ContextualHelpPropsMarkdown } from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import { mixpanelTrack } from "../../../../mixpanel"; @@ -32,15 +32,16 @@ import { IO_BARCODE_ALL_FORMATS, PagoPaBarcode } from "../../../barcode/types/IOBarcode"; -import { WalletPaymentRoutes } from "../../payment/navigation/routes"; -import { WalletBarcodeRoutes } from "../navigation/routes"; +import { usePagoPaPayment } from "../../checkout/hooks/usePagoPaPayment"; +import { PaymentsCheckoutRoutes } from "../../checkout/navigation/routes"; +import { PaymentsBarcodeRoutes } from "../navigation/routes"; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "wallet.QRtoPay.contextualHelpTitle", body: "wallet.QRtoPay.contextualHelpContent" }; -const WalletBarcodeScanScreen = () => { +const PaymentsBarcodeScanScreen = () => { const navigation = useNavigation>(); const dispatch = useIODispatch(); const { dataMatrixPosteEnabled } = useIOSelector( @@ -48,6 +49,9 @@ const WalletBarcodeScanScreen = () => { ); const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + const { startPaymentFlowWithRptId, isNewWalletSectionEnabled } = + usePagoPaPayment(); + const barcodeFormats: Array = IO_BARCODE_ALL_FORMATS.filter( format => (format === "DATA_MATRIX" ? dataMatrixPosteEnabled : true) ); @@ -77,8 +81,8 @@ const WalletBarcodeScanScreen = () => { } if (pagoPaBarcodes.length > 1) { - navigation.navigate(WalletBarcodeRoutes.WALLET_BARCODE_MAIN, { - screen: WalletBarcodeRoutes.WALLET_BARCODE_CHOICE, + navigation.navigate(PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR, { + screen: PaymentsBarcodeRoutes.PAYMENT_BARCODE_CHOICE, params: { barcodes: pagoPaBarcodes } @@ -89,31 +93,34 @@ const WalletBarcodeScanScreen = () => { const barcode = pagoPaBarcodes[0]; if (barcode.type === "PAGOPA") { - dispatch(paymentInitializeState()); - - switch (barcode.format) { - case "QR_CODE": - navigation.navigate(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, - params: { - initialAmount: barcode.amount, - rptId: barcode.rptId, - paymentStartOrigin: "qrcode_scan" - } - }); - break; - case "DATA_MATRIX": - void mixpanelTrack("WALLET_SCAN_POSTE_DATAMATRIX_SUCCESS"); - navigation.navigate(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, - params: { - initialAmount: barcode.amount, - rptId: barcode.rptId, - paymentStartOrigin: "poste_datamatrix_scan" - } - }); - - break; + if (isNewWalletSectionEnabled) { + startPaymentFlowWithRptId(barcode.rptId); + } else { + dispatch(paymentInitializeState()); + switch (barcode.format) { + case "QR_CODE": + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + initialAmount: barcode.amount, + rptId: barcode.rptId, + paymentStartOrigin: "qrcode_scan" + } + }); + break; + case "DATA_MATRIX": + void mixpanelTrack("WALLET_SCAN_POSTE_DATAMATRIX_SUCCESS"); + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + initialAmount: barcode.amount, + rptId: barcode.rptId, + paymentStartOrigin: "poste_datamatrix_scan" + } + }); + + break; + } } } }; @@ -133,8 +140,8 @@ const WalletBarcodeScanScreen = () => { analytics.trackBarcodeManualEntryPath("avviso"); if (isDesignSystemEnabled) { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_INPUT_NOTICE_NUMBER + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_INPUT_NOTICE_NUMBER }); } else { navigation.navigate(ROUTES.WALLET_NAVIGATOR, { @@ -177,4 +184,4 @@ const WalletBarcodeScanScreen = () => { ); }; -export { WalletBarcodeScanScreen }; +export { PaymentsBarcodeScanScreen }; diff --git a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx b/ts/features/payments/checkout/components/WalletPaymentConfirmContent.tsx similarity index 75% rename from ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx rename to ts/features/payments/checkout/components/WalletPaymentConfirmContent.tsx index 456914c71e8..9c62585a7ca 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentConfirmContent.tsx @@ -1,3 +1,4 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; import { Body, GradientScrollView, @@ -7,24 +8,21 @@ import { VSpacer } from "@pagopa/io-app-design-system"; 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 { format } from "../../../../utils/dates"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { capitalize } from "../../../../utils/strings"; import { WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL, getPaymentLogo } from "../../common/utils"; -import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { WalletPaymentStepEnum } from "../types"; +import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { walletPaymentSetCurrentStep } from "../store/actions/orchestration"; +import { walletPaymentPspListSelector } from "../store/selectors"; import { WalletPaymentTotalAmount } from "./WalletPaymentTotalAmount"; export type WalletPaymentConfirmContentProps = { @@ -42,7 +40,11 @@ export const WalletPaymentConfirmContent = ({ isLoading, onConfirm }: WalletPaymentConfirmContentProps) => { - const navigation = useNavigation>(); + const dispatch = useIODispatch(); + + const pspListPot = useIOSelector(walletPaymentPspListSelector); + + const pspList = pot.getOrElse(pspListPot, []); const taxFee = selectedPsp.taxPayerFee ?? 0; @@ -77,9 +79,11 @@ export const WalletPaymentConfirmContent = ({ title={getPaymentTitle(paymentMethodDetails)} subtitle={getPaymentSubtitle(paymentMethodDetails)} onPress={() => - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_METHOD - }) + dispatch( + walletPaymentSetCurrentStep( + WalletPaymentStepEnum.PICK_PAYMENT_METHOD + ) + ) } /> @@ -89,15 +93,15 @@ export const WalletPaymentConfirmContent = ({ iconName="psp" /> 1 ? I18n.t("payment.confirm.editButton") : undefined + } title={formatNumberCentsToAmount(taxFee, true, "right")} subtitle={`${I18n.t("payment.confirm.feeAppliedBy")} ${ selectedPsp.bundleName }`} onPress={() => - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP - }) + dispatch(walletPaymentSetCurrentStep(WalletPaymentStepEnum.PICK_PSP)) } /> @@ -120,21 +124,20 @@ export const WalletPaymentConfirmContent = ({ ); }; -const getPaymentSubtitle = (details: UIWalletInfoDetails): string => { - if (details.maskedPan !== undefined) { - return `${format(details.expiryDate, "MM/YY")}`; - } else if (details.maskedEmail !== undefined) { +const getPaymentSubtitle = ( + details: UIWalletInfoDetails +): string | undefined => { + if (details.maskedEmail !== undefined) { return I18n.t("wallet.onboarding.paypal.name"); } else if (details.maskedNumber !== undefined) { return `${details.bankName}`; } - - return ""; + return undefined; }; const getPaymentTitle = (details: UIWalletInfoDetails): string => { - if (details.maskedPan !== undefined) { - return `${capitalize(details.brand || "")} ••${details.maskedPan}`; + if (details.lastFourDigits !== undefined) { + return `${capitalize(details.brand || "")} ••${details.lastFourDigits}`; } else if (details.maskedEmail !== undefined) { return `${details.maskedEmail}`; } else if (details.maskedNumber !== undefined) { diff --git a/ts/features/walletV3/payment/components/WalletPaymentFailureDetail.tsx b/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx similarity index 98% rename from ts/features/walletV3/payment/components/WalletPaymentFailureDetail.tsx rename to ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx index 288590467c6..8e0c21a706f 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentFailureDetail.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx @@ -12,7 +12,7 @@ import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; -import { WalletPaymentFailure } from "../types/failure"; +import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; type Props = { failure: WalletPaymentFailure; diff --git a/ts/features/walletV3/payment/components/WalletPaymentFeedbackBanner.tsx b/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx similarity index 92% rename from ts/features/walletV3/payment/components/WalletPaymentFeedbackBanner.tsx rename to ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx index 3bd7baf454f..f8e0f0f885e 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentFeedbackBanner.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx @@ -17,6 +17,9 @@ const WalletPaymentFeebackBanner = () => { return openAuthenticationSession(WALLET_PAYMENT_FEEDBACK_URL, ""); }; + // This banner is temporarily disabled. Remove the next line to re-enable it + return null; + return ( <> diff --git a/ts/features/payments/checkout/components/WalletPaymentHeader.tsx b/ts/features/payments/checkout/components/WalletPaymentHeader.tsx new file mode 100644 index 00000000000..4b166ade0d1 --- /dev/null +++ b/ts/features/payments/checkout/components/WalletPaymentHeader.tsx @@ -0,0 +1,86 @@ +import { + ActionProp, + HeaderSecondLevel, + Stepper, + VSpacer +} from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; + +import React from "react"; +import { useStartSupportRequest } from "../../../../hooks/useStartSupportRequest"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler"; +import { walletPaymentSetCurrentStep } from "../store/actions/orchestration"; +import { useHardwareBackButton } from "../../../../hooks/useHardwareBackButton"; +import { WALLET_PAYMENT_STEP_MAX } from "../store/reducers"; +import { walletPaymentPspListSelector } from "../store/selectors"; +import { paymentsResetPaymentPspList } from "../store/actions/networking"; +import { WalletPaymentStepEnum } from "../types"; + +type WalletPaymentHeaderProps = { + currentStep: number; +}; + +const WalletPaymentHeader = ({ currentStep }: WalletPaymentHeaderProps) => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + const goBackHandler = useWalletPaymentGoBackHandler(); + + const pspListPot = useIOSelector(walletPaymentPspListSelector); + const pspList = pot.getOrElse(pspListPot, []); + + const startSupportRequest = useStartSupportRequest({ + faqCategories: ["payment"], + contextualHelp: emptyContextualHelp + }); + + const handleGoBack = React.useCallback(() => { + if (currentStep === WalletPaymentStepEnum.PICK_PAYMENT_METHOD) { + // If we are in the first step, if the goBackHandler is defined (payment was started) + // call it, otherwise use the default navigation goBack function + return (goBackHandler || navigation.goBack)(); + } else if ( + currentStep === WalletPaymentStepEnum.CONFIRM_TRANSACTION && + pspList.length === 1 + ) { + // If we are in the last step, if there is one PSP go back to the method selection + dispatch( + walletPaymentSetCurrentStep(WalletPaymentStepEnum.PICK_PAYMENT_METHOD) + ); + dispatch(paymentsResetPaymentPspList()); + } else { + // For any other step just go back 1 step + dispatch(walletPaymentSetCurrentStep(currentStep - 1)); + } + }, [navigation, dispatch, goBackHandler, currentStep, pspList]); + + useHardwareBackButton(() => { + handleGoBack(); + return true; + }); + + return ( + <> + + + + + ); +}; + +export { WalletPaymentHeader }; diff --git a/ts/features/walletV3/payment/components/WalletPaymentMissingMethodsError.tsx b/ts/features/payments/checkout/components/WalletPaymentMissingMethodsError.tsx similarity index 87% rename from ts/features/walletV3/payment/components/WalletPaymentMissingMethodsError.tsx rename to ts/features/payments/checkout/components/WalletPaymentMissingMethodsError.tsx index 6d49a16aab2..05498557365 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentMissingMethodsError.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentMissingMethodsError.tsx @@ -5,8 +5,8 @@ import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import { WalletOnboardingRoutes } from "../../onboarding/navigation/navigator"; import I18n from "../../../../i18n"; +import { PaymentsOnboardingRoutes } from "../../onboarding/navigation/routes"; const WalletPaymentMissingMethodsError = () => { const navigation = useNavigation>(); @@ -19,8 +19,8 @@ const WalletPaymentMissingMethodsError = () => { }, [navigation]); const handleAddMethod = () => { - navigation.push(WalletOnboardingRoutes.WALLET_ONBOARDING_MAIN, { - screen: WalletOnboardingRoutes.WALLET_ONBOARDING_SELECT_PAYMENT_METHOD + navigation.push(PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR, { + screen: PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD }); }; diff --git a/ts/features/walletV3/payment/components/WalletPaymentTotalAmount.tsx b/ts/features/payments/checkout/components/WalletPaymentTotalAmount.tsx similarity index 100% rename from ts/features/walletV3/payment/components/WalletPaymentTotalAmount.tsx rename to ts/features/payments/checkout/components/WalletPaymentTotalAmount.tsx diff --git a/ts/features/walletV3/payment/components/WalletPspListSkeleton.tsx b/ts/features/payments/checkout/components/WalletPspListSkeleton.tsx similarity index 100% rename from ts/features/walletV3/payment/components/WalletPspListSkeleton.tsx rename to ts/features/payments/checkout/components/WalletPspListSkeleton.tsx diff --git a/ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx b/ts/features/payments/checkout/hooks/useOnTransactionActivationEffect.tsx similarity index 90% rename from ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx rename to ts/features/payments/checkout/hooks/useOnTransactionActivationEffect.tsx index f4857eda2e4..76ff5d2df86 100644 --- a/ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx +++ b/ts/features/payments/checkout/hooks/useOnTransactionActivationEffect.tsx @@ -3,7 +3,7 @@ import { TransactionInfo } from "../../../../../definitions/pagopa/ecommerce/Tra import { TransactionStatusEnum } from "../../../../../definitions/pagopa/ecommerce/TransactionStatus"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { getGenericError } from "../../../../utils/errors"; -import { walletPaymentGetTransactionInfo } from "../store/actions/networking"; +import { paymentsGetPaymentTransactionInfoAction } from "../store/actions/networking"; import { walletPaymentTransactionSelector } from "../store/selectors"; const INITIAL_DELAY = 250; @@ -39,7 +39,7 @@ const useOnTransactionActivationEffect = (effect: EffectCallback) => { } else if (countRef.current > MAX_TRIES) { // The transaction is not yet ACTIVATED, and we exceeded the max retries dispatch( - walletPaymentGetTransactionInfo.failure( + paymentsGetPaymentTransactionInfoAction.failure( getGenericError(new Error("Max try reached")) ) ); @@ -49,7 +49,9 @@ const useOnTransactionActivationEffect = (effect: EffectCallback) => { const timeout = setTimeout(() => { delayRef.current *= 2; countRef.current += 1; - dispatch(walletPaymentGetTransactionInfo.request({ transactionId })); + dispatch( + paymentsGetPaymentTransactionInfoAction.request({ transactionId }) + ); }, delayRef.current); // Clean up the timeout to avoid memory leaks return () => { diff --git a/ts/features/payments/checkout/hooks/usePagoPaPayment.ts b/ts/features/payments/checkout/hooks/usePagoPaPayment.ts new file mode 100644 index 00000000000..e3d474958a1 --- /dev/null +++ b/ts/features/payments/checkout/hooks/usePagoPaPayment.ts @@ -0,0 +1,137 @@ +import { + RptId as PagoPaRptId, + RptIdFromString as PagoPaRptIdFromString, + PaymentNoticeNumberFromString +} from "@pagopa/io-pagopa-commons/lib/pagopa"; +import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; +import { sequenceS } from "fp-ts/lib/Apply"; +import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { RptId } from "../../../../../definitions/pagopa/ecommerce/RptId"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; +import { + PaymentInitStateParams, + initPaymentStateAction +} from "../store/actions/orchestration"; +import { isNewWalletSectionEnabledSelector } from "../../../../store/reducers/persistedPreferences"; + +type PagoPaPaymentParams = Omit; + +const DEFAULT_PAYMENT_PARAMS: PagoPaPaymentParams = {}; + +type PaymentData = { + paymentNoticeNumber: string; + organizationFiscalCode: string; +}; + +type UsePagoPaPayment = { + startPaymentFlow: (rptId: RptId, params?: PagoPaPaymentParams) => void; + startPaymentFlowWithRptId: ( + rptId: PagoPaRptId, + params?: PagoPaPaymentParams + ) => void; + startPaymentFlowWithData: ( + data: PaymentData, + params?: PagoPaPaymentParams + ) => void; + // This is a temporary flag to tell that the new payment flow is enabled and can be used + // Will be removed once the new wallet section is released + isNewWalletSectionEnabled: boolean; +}; + +/** + * A hook for initiating a PagoPA payment flow. + * This hook provides functions to start a payment flow using various input methods. + * @returns An object containing functions to start different types of payment flows. + */ +const usePagoPaPayment = (): UsePagoPaPayment => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + + // Checks if the new wallet section is enabled + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); + + /** + * Initializes the payment state based on the provided parameters. + * The initialization includes the store of the current route which allows the app to + * return to it when the payment flow is finished. + * @param {PagoPaPaymentParams} params - Parameters for initializing the payment state. + */ + const initPaymentState = (params: PagoPaPaymentParams) => { + dispatch(initPaymentStateAction(params)); + }; + + /** + * Initiates the payment flow using the provided RptId string and additional parameters. + * @param {RptId} rptId - The RptId for the payment flow. + * @param {PagoPaPaymentParams} params - Additional parameters for the payment flow. + */ + const startPaymentFlow = ( + rptId: RptId, + params: PagoPaPaymentParams = DEFAULT_PAYMENT_PARAMS + ) => { + initPaymentState(params); + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_DETAIL, + params: { + rptId + } + }); + }; + + /** + * Initiates the payment flow using the provided PagoPA RptId and additional parameters. + * @param {PagoPaRptId} rptId - The PagoPA RptId for the payment flow. + * @param {PagoPaPaymentParams} params - Additional parameters for the payment flow. + */ + const startPaymentFlowWithRptId = ( + rptId: PagoPaRptId, + params: PagoPaPaymentParams = DEFAULT_PAYMENT_PARAMS + ) => { + pipe( + O.fromNullable(rptId), + O.map(PagoPaRptIdFromString.encode), + O.map(RptId.decode), + O.chain(O.fromEither), + O.map(rptIdString => startPaymentFlow(rptIdString, params)) + ); + }; + + /** + * Initiates the payment flow using the provided payment data and additional parameters. + * @param {Object} data - Payment data containing the payment notice number and an organization fiscal code. + * @param {PagoPaPaymentParams} params - Additional parameters for the payment flow. + */ + const startPaymentFlowWithData = ( + data: PaymentData, + params: PagoPaPaymentParams = DEFAULT_PAYMENT_PARAMS + ) => { + pipe( + sequenceS(E.Monad)({ + paymentNoticeNumber: PaymentNoticeNumberFromString.decode( + data.paymentNoticeNumber + ), + organizationFiscalCode: OrganizationFiscalCode.decode( + data.organizationFiscalCode + ) + }), + E.map(PagoPaRptIdFromString.encode), + E.chain(RptId.decode), + E.map(rptIdString => startPaymentFlow(rptIdString, params)) + ); + }; + + return { + startPaymentFlow, + startPaymentFlowWithRptId, + startPaymentFlowWithData, + isNewWalletSectionEnabled + }; +}; + +export { usePagoPaPayment }; diff --git a/ts/features/walletV3/payment/hooks/usePaymentFailureSupportModal.tsx b/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx similarity index 93% rename from ts/features/walletV3/payment/hooks/usePaymentFailureSupportModal.tsx rename to ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx index 2dd47feab59..499e0f73a38 100644 --- a/ts/features/walletV3/payment/hooks/usePaymentFailureSupportModal.tsx +++ b/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx @@ -22,24 +22,27 @@ import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bott import { PAGOPA_SUPPORT_PHONE_NUMBER, addTicketCustomField, + appendLog, assistanceToolRemoteConfig, resetCustomFields, zendeskCategoryId, zendeskPaymentCategory, zendeskPaymentFailure, zendeskPaymentNav, - zendeskPaymentOrgFiscalCode + zendeskPaymentOrgFiscalCode, + zendeskPaymentStartOrigin } from "../../../../utils/supportAssistance"; import { zendeskSelectedCategory, zendeskSupportStart } from "../../../zendesk/store/actions"; +import { selectOngoingPaymentHistory } from "../../history/store/selectors"; import { walletPaymentRptIdSelector } from "../store/selectors"; import { WalletPaymentOutcome, getWalletPaymentOutcomeEnumByValue } from "../types/PaymentOutcomeEnum"; -import { WalletPaymentFailure } from "../types/failure"; +import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; type PaymentFailureSupportModalParams = { failure?: WalletPaymentFailure; @@ -60,6 +63,7 @@ const usePaymentFailureSupportModal = ({ const assistanceToolConfig = useIOSelector(assistanceToolConfigSelector); const choosenTool = assistanceToolRemoteConfig(assistanceToolConfig); const rptId = useIOSelector(walletPaymentRptIdSelector); + const paymentHistory = useIOSelector(selectOngoingPaymentHistory); const dispatch = useIODispatch(); const faultCodeDetail = @@ -73,9 +77,11 @@ const usePaymentFailureSupportModal = ({ addTicketCustomField(zendeskPaymentOrgFiscalCode, organizationFiscalCode); addTicketCustomField(zendeskPaymentNav, paymentNoticeNumber); addTicketCustomField(zendeskPaymentFailure, faultCodeDetail); - // TODO Add additional info for zendesk support (IOBP-484) - // addTicketCustomField(zendeskPaymentStartOrigin, "n/a"); - // appendLog(getPaymentHistoryDetails(payment)); + addTicketCustomField( + zendeskPaymentStartOrigin, + paymentHistory?.startOrigin || "n/a" + ); + appendLog(JSON.stringify(paymentHistory)); dispatch( zendeskSupportStart({ startingRoute: "n/a", diff --git a/ts/features/walletV3/payment/hooks/useSortPspBottomSheet.tsx b/ts/features/payments/checkout/hooks/useSortPspBottomSheet.tsx similarity index 100% rename from ts/features/walletV3/payment/hooks/useSortPspBottomSheet.tsx rename to ts/features/payments/checkout/hooks/useSortPspBottomSheet.tsx diff --git a/ts/features/walletV3/payment/hooks/useWalletPaymentAuthorizationModal.tsx b/ts/features/payments/checkout/hooks/useWalletPaymentAuthorizationModal.tsx similarity index 79% rename from ts/features/walletV3/payment/hooks/useWalletPaymentAuthorizationModal.tsx rename to ts/features/payments/checkout/hooks/useWalletPaymentAuthorizationModal.tsx index 347989d3e31..992335495f6 100644 --- a/ts/features/walletV3/payment/hooks/useWalletPaymentAuthorizationModal.tsx +++ b/ts/features/payments/checkout/hooks/useWalletPaymentAuthorizationModal.tsx @@ -7,9 +7,10 @@ import * as React from "react"; import URLParse from "url-parse"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { WALLET_WEBVIEW_OUTCOME_SCHEMA } from "../../common/utils/const"; +import { storePaymentOutcomeToHistory } from "../../history/store/actions"; import { WalletPaymentAuthorizePayload, - walletPaymentAuthorization + paymentsStartPaymentAuthorizationAction } from "../store/actions/networking"; import { walletPaymentAuthorizationUrlSelector } from "../store/selectors"; import { @@ -19,7 +20,6 @@ import { type Props = { onAuthorizationOutcome: (outcome: WalletPaymentOutcome) => void; - onDismiss: () => void; }; export type WalletPaymentAuthorizationModal = { @@ -30,10 +30,10 @@ export type WalletPaymentAuthorizationModal = { }; export const useWalletPaymentAuthorizationModal = ({ - onAuthorizationOutcome, - onDismiss + onAuthorizationOutcome }: Props): WalletPaymentAuthorizationModal => { const dispatch = useIODispatch(); + const authorizationUrlPot = useIOSelector( walletPaymentAuthorizationUrlSelector ); @@ -43,6 +43,14 @@ export const useWalletPaymentAuthorizationModal = ({ const isLoading = pot.isLoading(authorizationUrlPot); const isError = pot.isError(authorizationUrlPot); + const handleAuthorizationOutcome = React.useCallback( + (outcome: WalletPaymentOutcome) => { + onAuthorizationOutcome(outcome); + dispatch(storePaymentOutcomeToHistory(outcome)); + }, + [onAuthorizationOutcome, dispatch] + ); + const handleAuthorizationResult = React.useCallback( (resultUrl: string) => { const outcome = pipe( @@ -51,9 +59,9 @@ export const useWalletPaymentAuthorizationModal = ({ WalletPaymentOutcome.decode, E.getOrElse(() => WalletPaymentOutcomeEnum.GENERIC_ERROR) ); - onAuthorizationOutcome(outcome); + handleAuthorizationOutcome(outcome); }, - [onAuthorizationOutcome] + [handleAuthorizationOutcome] ); React.useEffect(() => { @@ -75,9 +83,9 @@ export const useWalletPaymentAuthorizationModal = ({ ); }, () => { - onDismiss(); - dispatch(walletPaymentAuthorization.cancel()); - setIsPendingAuthorization(false); + handleAuthorizationOutcome( + WalletPaymentOutcomeEnum.CANCELED_BY_USER + ); } ) ), @@ -89,21 +97,21 @@ export const useWalletPaymentAuthorizationModal = ({ isPendingAuthorization, authorizationUrlPot, handleAuthorizationResult, - onDismiss, + handleAuthorizationOutcome, dispatch ]); React.useEffect( () => () => { setIsPendingAuthorization(false); - dispatch(walletPaymentAuthorization.cancel()); + dispatch(paymentsStartPaymentAuthorizationAction.cancel()); }, [dispatch] ); const startPaymentAuthorizaton = (payload: WalletPaymentAuthorizePayload) => { setIsPendingAuthorization(false); - dispatch(walletPaymentAuthorization.request(payload)); + dispatch(paymentsStartPaymentAuthorizationAction.request(payload)); }; return { diff --git a/ts/features/walletV3/payment/hooks/useWalletPaymentGoBackHandler.tsx b/ts/features/payments/checkout/hooks/useWalletPaymentGoBackHandler.tsx similarity index 84% rename from ts/features/walletV3/payment/hooks/useWalletPaymentGoBackHandler.tsx rename to ts/features/payments/checkout/hooks/useWalletPaymentGoBackHandler.tsx index 8101abf6fca..13fc9c328de 100644 --- a/ts/features/walletV3/payment/hooks/useWalletPaymentGoBackHandler.tsx +++ b/ts/features/payments/checkout/hooks/useWalletPaymentGoBackHandler.tsx @@ -7,9 +7,9 @@ import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { walletPaymentDeleteTransaction } from "../store/actions/networking"; +import { paymentsDeleteTransactionAction } from "../store/actions/networking"; import { walletPaymentTransactionSelector } from "../store/selectors"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; const useWalletPaymentGoBackHandler = () => { @@ -29,9 +29,9 @@ const useWalletPaymentGoBackHandler = () => { const { transactionId } = transactionPot.value; const handleConfirmAbort = () => { - dispatch(walletPaymentDeleteTransaction.request(transactionId)); - navigation.push(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME, + dispatch(paymentsDeleteTransactionAction.request(transactionId)); + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, params: { outcome: WalletPaymentOutcomeEnum.CANCELED_BY_USER } diff --git a/ts/features/payments/checkout/navigation/navigator.tsx b/ts/features/payments/checkout/navigation/navigator.tsx new file mode 100644 index 00000000000..aeca8d42d82 --- /dev/null +++ b/ts/features/payments/checkout/navigation/navigator.tsx @@ -0,0 +1,76 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import { WalletPaymentDetailScreen } from "../screens/WalletPaymentDetailScreen"; +import { WalletPaymentInputFiscalCodeScreen } from "../screens/WalletPaymentInputFiscalCodeScreen"; +import { WalletPaymentInputNoticeNumberScreen } from "../screens/WalletPaymentInputNoticeNumberScreen"; +import { WalletPaymentMakeScreen } from "../screens/WalletPaymentMakeScreen"; +import { WalletPaymentOutcomeScreen } from "../screens/WalletPaymentOutcomeScreen"; +import { PaymentsCheckoutParamsList } from "./params"; +import { PaymentsCheckoutRoutes } from "./routes"; + +const Stack = createStackNavigator(); + +export const PaymentsCheckoutNavigator = () => ( + + + + + + + +); + +export type PaymentsCheckoutStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type PaymentsCheckoutStackNavigation = + PaymentsCheckoutStackNavigationProp< + PaymentsCheckoutParamsList, + keyof PaymentsCheckoutParamsList + >; diff --git a/ts/features/payments/checkout/navigation/params.ts b/ts/features/payments/checkout/navigation/params.ts new file mode 100644 index 00000000000..6a0639873ab --- /dev/null +++ b/ts/features/payments/checkout/navigation/params.ts @@ -0,0 +1,13 @@ +import { WalletPaymentDetailScreenNavigationParams } from "../screens/WalletPaymentDetailScreen"; +import { WalletPaymentInputFiscalCodeScreenNavigationParams } from "../screens/WalletPaymentInputFiscalCodeScreen"; +import { WalletPaymentOutcomeScreenNavigationParams } from "../screens/WalletPaymentOutcomeScreen"; +import { PaymentsCheckoutRoutes } from "./routes"; + +export type PaymentsCheckoutParamsList = { + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR]: undefined; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_INPUT_NOTICE_NUMBER]: undefined; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_INPUT_FISCAL_CODE]: WalletPaymentInputFiscalCodeScreenNavigationParams; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_DETAIL]: WalletPaymentDetailScreenNavigationParams; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_MAKE]: undefined; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME]: WalletPaymentOutcomeScreenNavigationParams; +}; diff --git a/ts/features/payments/checkout/navigation/routes.ts b/ts/features/payments/checkout/navigation/routes.ts new file mode 100644 index 00000000000..024828de2e3 --- /dev/null +++ b/ts/features/payments/checkout/navigation/routes.ts @@ -0,0 +1,8 @@ +export const PaymentsCheckoutRoutes = { + PAYMENT_CHECKOUT_NAVIGATOR: "PAYMENT_CHECKOUT_NAVIGATOR", + PAYMENT_CHECKOUT_INPUT_NOTICE_NUMBER: "PAYMENT_CHECKOUT_INPUT_NOTICE_NUMBER", + PAYMENT_CHECKOUT_INPUT_FISCAL_CODE: "PAYMENT_CHECKOUT_INPUT_FISCAL_CODE", + PAYMENT_CHECKOUT_DETAIL: "PAYMENT_CHECKOUT_DETAIL", + PAYMENT_CHECKOUT_MAKE: "PAYMENT_CHECKOUT_MAKE", + PAYMENT_CHECKOUT_OUTCOME: "PAYMENT_CHECKOUT_OUTCOME" +} as const; diff --git a/ts/features/walletV3/payment/saga/index.ts b/ts/features/payments/checkout/saga/index.ts similarity index 68% rename from ts/features/walletV3/payment/saga/index.ts rename to ts/features/payments/checkout/saga/index.ts index 450a3a8a356..3945fabdef0 100644 --- a/ts/features/walletV3/payment/saga/index.ts +++ b/ts/features/payments/checkout/saga/index.ts @@ -1,86 +1,84 @@ import { SagaIterator } from "redux-saga"; import { takeLatest } from "typed-redux-saga/macro"; -import { WalletClient } from "../../common/api/client"; -import { PaymentClient } from "../api/client"; +import { PaymentClient } from "../../common/api/client"; import { - walletPaymentAuthorization, - walletPaymentCalculateFees, - walletPaymentCreateTransaction, - walletPaymentDeleteTransaction, - walletPaymentGetAllMethods, - walletPaymentGetDetails, - walletPaymentGetTransactionInfo, - walletPaymentGetUserWallets, - walletPaymentNewSessionToken + paymentsCalculatePaymentFeesAction, + paymentsCreateTransactionAction, + paymentsDeleteTransactionAction, + paymentsGetNewSessionTokenAction, + paymentsGetPaymentDetailsAction, + paymentsGetPaymentMethodsAction, + paymentsGetPaymentTransactionInfoAction, + paymentsGetPaymentUserMethodsAction, + paymentsStartPaymentAuthorizationAction } from "../store/actions/networking"; +import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization"; import { handleWalletPaymentCalculateFees } from "./networking/handleWalletPaymentCalculateFees"; import { handleWalletPaymentCreateTransaction } from "./networking/handleWalletPaymentCreateTransaction"; +import { handleWalletPaymentDeleteTransaction } from "./networking/handleWalletPaymentDeleteTransaction"; import { handleWalletPaymentGetAllMethods } from "./networking/handleWalletPaymentGetAllMethods"; import { handleWalletPaymentGetDetails } from "./networking/handleWalletPaymentGetDetails"; +import { handleWalletPaymentGetTransactionInfo } from "./networking/handleWalletPaymentGetTransactionInfo"; import { handleWalletPaymentGetUserWallets } from "./networking/handleWalletPaymentGetUserWallets"; -import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization"; -import { handleWalletPaymentDeleteTransaction } from "./networking/handleWalletPaymentDeleteTransaction"; import { handleWalletPaymentNewSessionToken } from "./networking/handleWalletPaymentNewSessionToken"; -import { handleWalletPaymentGetTransactionInfo } from "./networking/handleWalletPaymentGetTransactionInfo"; /** * Handle the pagoPA payments requests * @param bearerToken */ -export function* watchWalletPaymentSaga( - walletClient: WalletClient, +export function* watchPaymentsCheckoutSaga( paymentClient: PaymentClient ): SagaIterator { yield* takeLatest( - walletPaymentNewSessionToken.request, + paymentsGetNewSessionTokenAction.request, handleWalletPaymentNewSessionToken, paymentClient.newSessionToken ); yield* takeLatest( - walletPaymentGetDetails.request, + paymentsGetPaymentDetailsAction.request, handleWalletPaymentGetDetails, paymentClient.getPaymentRequestInfo ); yield* takeLatest( - walletPaymentGetAllMethods.request, + paymentsGetPaymentMethodsAction.request, handleWalletPaymentGetAllMethods, - walletClient.getAllPaymentMethods + paymentClient.getAllPaymentMethods ); yield* takeLatest( - walletPaymentGetUserWallets.request, + paymentsGetPaymentUserMethodsAction.request, handleWalletPaymentGetUserWallets, - walletClient.getWalletsByIdUser + paymentClient.getWalletsByIdUser ); yield* takeLatest( - walletPaymentCalculateFees.request, + paymentsCalculatePaymentFeesAction.request, handleWalletPaymentCalculateFees, paymentClient.calculateFees ); yield* takeLatest( - walletPaymentCreateTransaction.request, + paymentsCreateTransactionAction.request, handleWalletPaymentCreateTransaction, paymentClient.newTransaction ); yield* takeLatest( - walletPaymentGetTransactionInfo.request, + paymentsGetPaymentTransactionInfoAction.request, handleWalletPaymentGetTransactionInfo, paymentClient.getTransactionInfo ); yield* takeLatest( - walletPaymentDeleteTransaction.request, + paymentsDeleteTransactionAction.request, handleWalletPaymentDeleteTransaction, paymentClient.requestTransactionUserCancellation ); yield* takeLatest( - walletPaymentAuthorization.request, + paymentsStartPaymentAuthorizationAction.request, handleWalletPaymentAuthorization, paymentClient.requestTransactionAuthorization ); diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts similarity index 79% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts index 1ae6c698d1b..139f2aab663 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentAuthorization.test.ts @@ -8,7 +8,7 @@ import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; import { WalletPaymentAuthorizePayload, - walletPaymentAuthorization + paymentsStartPaymentAuthorizationAction } from "../../../store/actions/networking"; import { handleWalletPaymentAuthorization } from "../handleWalletPaymentAuthorization"; import { selectWalletPaymentSessionToken } from "../../../store/selectors"; @@ -25,7 +25,7 @@ describe("Test handleWalletPaymentAuthorization saga", () => { const T_SESSION_TOKEN = "ABCD"; it(`should put ${getType( - walletPaymentAuthorization.success + paymentsStartPaymentAuthorizationAction.success )} when requestTransactionAuthorization is 200`, () => { const mockRequestTransactionAuthorization = jest.fn(); const requestTransactionAuthorizationResponse: RequestAuthorizationResponse = @@ -37,7 +37,9 @@ describe("Test handleWalletPaymentAuthorization saga", () => { testSaga( handleWalletPaymentAuthorization, mockRequestTransactionAuthorization, - walletPaymentAuthorization.request(requestTransactionAuthorizationPayload) + paymentsStartPaymentAuthorizationAction.request( + requestTransactionAuthorizationPayload + ) ) .next() .select(selectWalletPaymentSessionToken) @@ -45,7 +47,7 @@ describe("Test handleWalletPaymentAuthorization saga", () => { .call( withRefreshApiCall, mockRequestTransactionAuthorization(), - walletPaymentAuthorization.request( + paymentsStartPaymentAuthorizationAction.request( requestTransactionAuthorizationPayload ) ) @@ -53,7 +55,7 @@ describe("Test handleWalletPaymentAuthorization saga", () => { E.right({ status: 200, value: requestTransactionAuthorizationResponse }) ) .put( - walletPaymentAuthorization.success( + paymentsStartPaymentAuthorizationAction.success( requestTransactionAuthorizationResponse ) ) @@ -62,14 +64,16 @@ describe("Test handleWalletPaymentAuthorization saga", () => { }); it(`should put ${getType( - walletPaymentAuthorization.failure + paymentsStartPaymentAuthorizationAction.failure )} when requestTransactionAuthorization is not 200`, () => { const mockRequestTransactionAuthorization = jest.fn(); testSaga( handleWalletPaymentAuthorization, mockRequestTransactionAuthorization, - walletPaymentAuthorization.request(requestTransactionAuthorizationPayload) + paymentsStartPaymentAuthorizationAction.request( + requestTransactionAuthorizationPayload + ) ) .next() .select(selectWalletPaymentSessionToken) @@ -77,13 +81,13 @@ describe("Test handleWalletPaymentAuthorization saga", () => { .call( withRefreshApiCall, mockRequestTransactionAuthorization(), - walletPaymentAuthorization.request( + paymentsStartPaymentAuthorizationAction.request( requestTransactionAuthorizationPayload ) ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentAuthorization.failure( + paymentsStartPaymentAuthorizationAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -92,14 +96,16 @@ describe("Test handleWalletPaymentAuthorization saga", () => { }); it(`should put ${getType( - walletPaymentAuthorization.failure + paymentsStartPaymentAuthorizationAction.failure )} when requestTransactionAuthorization encoders returns an error`, () => { const mockRequestTransactionAuthorization = jest.fn(); testSaga( handleWalletPaymentAuthorization, mockRequestTransactionAuthorization, - walletPaymentAuthorization.request(requestTransactionAuthorizationPayload) + paymentsStartPaymentAuthorizationAction.request( + requestTransactionAuthorizationPayload + ) ) .next() .select(selectWalletPaymentSessionToken) @@ -107,13 +113,13 @@ describe("Test handleWalletPaymentAuthorization saga", () => { .call( withRefreshApiCall, mockRequestTransactionAuthorization(), - walletPaymentAuthorization.request( + paymentsStartPaymentAuthorizationAction.request( requestTransactionAuthorizationPayload ) ) .next(E.left([])) .put( - walletPaymentAuthorization.failure({ + paymentsStartPaymentAuthorizationAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts similarity index 57% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts index 9488931478e..6eb80bb905d 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCalculateFees.test.ts @@ -6,11 +6,12 @@ import { PaymentMethodStatusEnum } from "../../../../../../../definitions/pagopa import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentCalculateFees } from "../../../store/actions/networking"; +import { paymentsCalculatePaymentFeesAction } from "../../../store/actions/networking"; import { handleWalletPaymentCalculateFees } from "../handleWalletPaymentCalculateFees"; import { CalculateFeeRequest } from "../../../../../../../definitions/pagopa/ecommerce/CalculateFeeRequest"; import { selectWalletPaymentSessionToken } from "../../../store/selectors"; import { preferredLanguageSelector } from "../../../../../../store/reducers/persistedPreferences"; +import { selectPaymentPspAction } from "../../../store/actions/orchestration"; describe("Test handleWalletPaymentCalculateFees saga", () => { const calculateFeesPayload: CalculateFeeRequest & { @@ -22,15 +23,19 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { const T_SESSION_TOKEN = "ABCD"; it(`should put ${getType( - walletPaymentCalculateFees.success - )} when calculateFees is 200`, () => { + paymentsCalculatePaymentFeesAction.success + )} when calculateFees is 200 and bundles are more than one`, () => { const mockCalculateFees = jest.fn(); const calculateFeesResponse: CalculateFeeResponse = { bundles: [ { idBundle: "idBundle" + }, + { + idBundle: "idBundle2" } ], + asset: "asset", paymentMethodDescription: "paymentMethodDescription", paymentMethodName: "paymentMethodName", paymentMethodStatus: PaymentMethodStatusEnum.ENABLED @@ -39,7 +44,7 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { testSaga( handleWalletPaymentCalculateFees, mockCalculateFees, - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next() .select(preferredLanguageSelector) @@ -49,24 +54,24 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { .call( withRefreshApiCall, mockCalculateFees(), - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next(E.right({ status: 200, value: calculateFeesResponse })) .next() - .put(walletPaymentCalculateFees.success(calculateFeesResponse)) + .put(paymentsCalculatePaymentFeesAction.success(calculateFeesResponse)) .next() .isDone(); }); it(`should put ${getType( - walletPaymentCalculateFees.failure + paymentsCalculatePaymentFeesAction.failure )} when calculateFees is not 200`, () => { const mockCalculateFees = jest.fn(); testSaga( handleWalletPaymentCalculateFees, mockCalculateFees, - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next() .select(preferredLanguageSelector) @@ -76,11 +81,11 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { .call( withRefreshApiCall, mockCalculateFees(), - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentCalculateFees.failure( + paymentsCalculatePaymentFeesAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -89,14 +94,14 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { }); it(`should put ${getType( - walletPaymentCalculateFees.failure + paymentsCalculatePaymentFeesAction.failure )} when calculateFees encoders returns an error`, () => { const mockCalculateFees = jest.fn(); testSaga( handleWalletPaymentCalculateFees, mockCalculateFees, - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next() .select(preferredLanguageSelector) @@ -106,15 +111,55 @@ describe("Test handleWalletPaymentCalculateFees saga", () => { .call( withRefreshApiCall, mockCalculateFees(), - walletPaymentCalculateFees.request(calculateFeesPayload) + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) ) .next(E.left([])) .put( - walletPaymentCalculateFees.failure({ + paymentsCalculatePaymentFeesAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) .next() .isDone(); }); + + it(`should put ${getType( + selectPaymentPspAction + )} with first psp in the list when calculateFees is 200 and bundles is only one in list`, () => { + const mockCalculateFees = jest.fn(); + const calculateFeesResponse: CalculateFeeResponse = { + bundles: [ + { + idBundle: "idBundle" + } + ], + asset: "asset", + paymentMethodDescription: "paymentMethodDescription", + paymentMethodName: "paymentMethodName", + paymentMethodStatus: PaymentMethodStatusEnum.ENABLED + }; + + testSaga( + handleWalletPaymentCalculateFees, + mockCalculateFees, + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) + ) + .next() + .select(preferredLanguageSelector) + .next("IT") + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) + .call( + withRefreshApiCall, + mockCalculateFees(), + paymentsCalculatePaymentFeesAction.request(calculateFeesPayload) + ) + .next(E.right({ status: 200, value: calculateFeesResponse })) + .next() + .put(selectPaymentPspAction(calculateFeesResponse.bundles[0])) + .next() + .put(paymentsCalculatePaymentFeesAction.success(calculateFeesResponse)) + .next() + .isDone(); + }); }); diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts similarity index 79% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts index f70d052038e..97136b07f2f 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentCreateTransaction.test.ts @@ -9,7 +9,7 @@ import { TransactionStatusEnum } from "../../../../../../../definitions/pagopa/e import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentCreateTransaction } from "../../../store/actions/networking"; +import { paymentsCreateTransactionAction } from "../../../store/actions/networking"; import { handleWalletPaymentCreateTransaction } from "../handleWalletPaymentCreateTransaction"; import { selectWalletPaymentSessionToken } from "../../../store/selectors"; @@ -25,7 +25,7 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { const T_SESSION_TOKEN = "ABCD"; it(`should put ${getType( - walletPaymentCreateTransaction.success + paymentsCreateTransactionAction.success )} when newTransaction is 200`, () => { const mockNewTransaction = jest.fn(); const newTransactionResponse: NewTransactionResponse = { @@ -42,7 +42,7 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { testSaga( handleWalletPaymentCreateTransaction, mockNewTransaction, - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next() .select(selectWalletPaymentSessionToken) @@ -50,23 +50,23 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { .call( withRefreshApiCall, mockNewTransaction(), - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next(E.right({ status: 200, value: newTransactionResponse })) - .put(walletPaymentCreateTransaction.success(newTransactionResponse)) + .put(paymentsCreateTransactionAction.success(newTransactionResponse)) .next() .isDone(); }); it(`should put ${getType( - walletPaymentCreateTransaction.failure + paymentsCreateTransactionAction.failure )} when newTransaction is not 200`, () => { const mockNewTransaction = jest.fn(); testSaga( handleWalletPaymentCreateTransaction, mockNewTransaction, - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next() .select(selectWalletPaymentSessionToken) @@ -74,11 +74,11 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { .call( withRefreshApiCall, mockNewTransaction(), - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentCreateTransaction.failure( + paymentsCreateTransactionAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -87,14 +87,14 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { }); it(`should put ${getType( - walletPaymentCreateTransaction.failure + paymentsCreateTransactionAction.failure )} when newTransaction encoders returns an error`, () => { const mockNewTransaction = jest.fn(); testSaga( handleWalletPaymentCreateTransaction, mockNewTransaction, - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next() .select(selectWalletPaymentSessionToken) @@ -102,11 +102,11 @@ describe("Test handleWalletPaymentCreateTransaction saga", () => { .call( withRefreshApiCall, mockNewTransaction(), - walletPaymentCreateTransaction.request(newTransactionPayload) + paymentsCreateTransactionAction.request(newTransactionPayload) ) .next(E.left([])) .put( - walletPaymentCreateTransaction.failure({ + paymentsCreateTransactionAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts similarity index 64% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts index 6644848ac9f..f46527b7fe5 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetAllMethods.test.ts @@ -2,17 +2,21 @@ import * as E from "fp-ts/lib/Either"; import { testSaga } from "redux-saga-test-plan"; import { getType } from "typesafe-actions"; import { Range } from "../../../../../../../definitions/pagopa/ecommerce/Range"; -import { PaymentMethodStatusEnum } from "../../../../../../../definitions/pagopa/walletv3/PaymentMethodStatus"; -import { PaymentMethodsResponse } from "../../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; +import { PaymentMethodStatusEnum } from "../../../../../../../definitions/pagopa/ecommerce/PaymentMethodStatus"; +import { PaymentMethodsResponse } from "../../../../../../../definitions/pagopa/ecommerce/PaymentMethodsResponse"; import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentGetAllMethods } from "../../../store/actions/networking"; +import { paymentsGetPaymentMethodsAction } from "../../../store/actions/networking"; import { handleWalletPaymentGetAllMethods } from "../handleWalletPaymentGetAllMethods"; +import { PaymentMethodManagementTypeEnum } from "../../../../../../../definitions/pagopa/ecommerce/PaymentMethodManagementType"; +import { selectWalletPaymentSessionToken } from "../../../store/selectors"; describe("Test handleWalletPaymentGetAllMethods saga", () => { + const T_SESSION_TOKEN = "ABCD"; + it(`should put ${getType( - walletPaymentGetAllMethods.success + paymentsGetPaymentMethodsAction.success )} when getAllPaymentMethods is 200`, () => { const mockGetAllPaymentMethods = jest.fn(); const getAllPaymentMethodsResponse: PaymentMethodsResponse = { @@ -28,7 +32,8 @@ describe("Test handleWalletPaymentGetAllMethods saga", () => { max: 10 as Range["max"] } ], - status: PaymentMethodStatusEnum.ENABLED + status: PaymentMethodStatusEnum.ENABLED, + methodManagement: PaymentMethodManagementTypeEnum.ONBOARDABLE } ] }; @@ -36,39 +41,45 @@ describe("Test handleWalletPaymentGetAllMethods saga", () => { testSaga( handleWalletPaymentGetAllMethods, mockGetAllPaymentMethods, - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetAllPaymentMethods(), - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next(E.right({ status: 200, value: getAllPaymentMethodsResponse })) - .put(walletPaymentGetAllMethods.success(getAllPaymentMethodsResponse)) + .put( + paymentsGetPaymentMethodsAction.success(getAllPaymentMethodsResponse) + ) .next() .isDone(); }); it(`should put ${getType( - walletPaymentGetAllMethods.failure + paymentsGetPaymentMethodsAction.failure )} when getAllPaymentMethods is not 200`, () => { const mockGetAllPaymentMethods = jest.fn(); testSaga( handleWalletPaymentGetAllMethods, mockGetAllPaymentMethods, - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetAllPaymentMethods(), - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentGetAllMethods.failure( + paymentsGetPaymentMethodsAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -77,24 +88,26 @@ describe("Test handleWalletPaymentGetAllMethods saga", () => { }); it(`should put ${getType( - walletPaymentGetAllMethods.failure + paymentsGetPaymentMethodsAction.failure )} when getAllPaymentMethods encoders returns an error`, () => { const mockGetAllPaymentMethods = jest.fn(); testSaga( handleWalletPaymentGetAllMethods, mockGetAllPaymentMethods, - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetAllPaymentMethods(), - walletPaymentGetAllMethods.request() + paymentsGetPaymentMethodsAction.request() ) .next(E.left([])) .put( - walletPaymentGetAllMethods.failure({ + paymentsGetPaymentMethodsAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts similarity index 78% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts index d496e799c40..32a7182b806 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetDetails.test.ts @@ -6,7 +6,7 @@ import { RptId } from "../../../../../../../definitions/pagopa/ecommerce/RptId"; import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentGetDetails } from "../../../store/actions/networking"; +import { paymentsGetPaymentDetailsAction } from "../../../store/actions/networking"; import { selectWalletPaymentSessionToken } from "../../../store/selectors"; import { handleWalletPaymentGetDetails } from "../handleWalletPaymentGetDetails"; @@ -15,7 +15,7 @@ describe("Test handleWalletPaymentGetDetails saga", () => { const T_SESSION_TOKEN = "ABCD"; it(`should put ${getType( - walletPaymentGetDetails.success + paymentsGetPaymentDetailsAction.success )} when getPaymentRequestInfo is 200`, () => { const mockGetPaymentRequestInfo = jest.fn(); const getPaymentRequestInfoResponse: PaymentRequestsGetResponse = { @@ -25,7 +25,7 @@ describe("Test handleWalletPaymentGetDetails saga", () => { testSaga( handleWalletPaymentGetDetails, mockGetPaymentRequestInfo, - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next() .select(selectWalletPaymentSessionToken) @@ -33,23 +33,25 @@ describe("Test handleWalletPaymentGetDetails saga", () => { .call( withRefreshApiCall, mockGetPaymentRequestInfo(), - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next(E.right({ status: 200, value: getPaymentRequestInfoResponse })) - .put(walletPaymentGetDetails.success(getPaymentRequestInfoResponse)) + .put( + paymentsGetPaymentDetailsAction.success(getPaymentRequestInfoResponse) + ) .next() .isDone(); }); it(`should put ${getType( - walletPaymentGetDetails.failure + paymentsGetPaymentDetailsAction.failure )} when getPaymentRequestInfo is not 200`, () => { const mockGetPaymentRequestInfo = jest.fn(); testSaga( handleWalletPaymentGetDetails, mockGetPaymentRequestInfo, - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next() .select(selectWalletPaymentSessionToken) @@ -57,11 +59,11 @@ describe("Test handleWalletPaymentGetDetails saga", () => { .call( withRefreshApiCall, mockGetPaymentRequestInfo(), - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentGetDetails.failure( + paymentsGetPaymentDetailsAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -70,14 +72,14 @@ describe("Test handleWalletPaymentGetDetails saga", () => { }); it(`should put ${getType( - walletPaymentGetDetails.failure + paymentsGetPaymentDetailsAction.failure )} when getPaymentRequestInfo encoders returns an error`, () => { const mockGetPaymentRequestInfo = jest.fn(); testSaga( handleWalletPaymentGetDetails, mockGetPaymentRequestInfo, - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next() .select(selectWalletPaymentSessionToken) @@ -85,11 +87,11 @@ describe("Test handleWalletPaymentGetDetails saga", () => { .call( withRefreshApiCall, mockGetPaymentRequestInfo(), - walletPaymentGetDetails.request(rptId) + paymentsGetPaymentDetailsAction.request(rptId) ) .next(E.left([])) .put( - walletPaymentGetDetails.failure({ + paymentsGetPaymentDetailsAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts similarity index 66% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts index ef5a876faf6..29b4a45dceb 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentGetUserWallets.test.ts @@ -2,16 +2,19 @@ import * as E from "fp-ts/lib/Either"; import { testSaga } from "redux-saga-test-plan"; import { getType } from "typesafe-actions"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentGetUserWallets } from "../../../store/actions/networking"; +import { paymentsGetPaymentUserMethodsAction } from "../../../store/actions/networking"; import { handleWalletPaymentGetUserWallets } from "../handleWalletPaymentGetUserWallets"; -import { Wallets } from "../../../../../../../definitions/pagopa/walletv3/Wallets"; +import { Wallets } from "../../../../../../../definitions/pagopa/ecommerce/Wallets"; import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { WalletStatusEnum } from "../../../../../../../definitions/pagopa/walletv3/WalletStatus"; +import { selectWalletPaymentSessionToken } from "../../../store/selectors"; describe("Test handleWalletPaymentGetUserWallets saga", () => { + const T_SESSION_TOKEN = "ABCD"; + it(`should put ${getType( - walletPaymentGetUserWallets.success + paymentsGetPaymentUserMethodsAction.success )} when getWalletsByIdUser is 200`, () => { const mockGetWalletsByIdUser = jest.fn(); const getWalletsByIdUserResponse: Wallets = { @@ -20,7 +23,8 @@ describe("Test handleWalletPaymentGetUserWallets saga", () => { walletId: "walletId", creationDate: new Date(), paymentMethodId: "paymentMethodId", - services: [], + paymentMethodAsset: "paymentMethodAsset", + applications: [], status: WalletStatusEnum.CREATED, updateDate: new Date() } @@ -30,39 +34,45 @@ describe("Test handleWalletPaymentGetUserWallets saga", () => { testSaga( handleWalletPaymentGetUserWallets, mockGetWalletsByIdUser, - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetWalletsByIdUser(), - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next(E.right({ status: 200, value: getWalletsByIdUserResponse })) - .put(walletPaymentGetUserWallets.success(getWalletsByIdUserResponse)) + .put( + paymentsGetPaymentUserMethodsAction.success(getWalletsByIdUserResponse) + ) .next() .isDone(); }); it(`should put ${getType( - walletPaymentGetUserWallets.failure + paymentsGetPaymentUserMethodsAction.failure )} when getWalletsByIdUser is not 200`, () => { const mockGetWalletsByIdUser = jest.fn(); testSaga( handleWalletPaymentGetUserWallets, mockGetWalletsByIdUser, - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetWalletsByIdUser(), - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentGetUserWallets.failure( + paymentsGetPaymentUserMethodsAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -71,24 +81,26 @@ describe("Test handleWalletPaymentGetUserWallets saga", () => { }); it(`should put ${getType( - walletPaymentGetUserWallets.failure + paymentsGetPaymentUserMethodsAction.failure )} when getWalletsByIdUser encoders returns an error`, () => { const mockGetWalletsByIdUser = jest.fn(); testSaga( handleWalletPaymentGetUserWallets, mockGetWalletsByIdUser, - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next() + .select(selectWalletPaymentSessionToken) + .next(T_SESSION_TOKEN) .call( withRefreshApiCall, mockGetWalletsByIdUser(), - walletPaymentGetUserWallets.request() + paymentsGetPaymentUserMethodsAction.request() ) .next(E.left([])) .put( - walletPaymentGetUserWallets.failure({ + paymentsGetPaymentUserMethodsAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts similarity index 75% rename from ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts rename to ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts index 2e27ca3fb43..c9c804e44ff 100644 --- a/ts/features/walletV3/payment/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts +++ b/ts/features/payments/checkout/saga/networking/__tests__/handleWalletPaymentNewSessionToken.test.ts @@ -5,12 +5,12 @@ import { NewSessionTokenResponse } from "../../../../../../../definitions/pagopa import { getGenericError } from "../../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../../fastLogin/saga/utils"; -import { walletPaymentNewSessionToken } from "../../../store/actions/networking"; +import { paymentsGetNewSessionTokenAction } from "../../../store/actions/networking"; import { handleWalletPaymentNewSessionToken } from "../handleWalletPaymentNewSessionToken"; describe("Test handleWalletPaymentNewSessionToken saga", () => { it(`should put ${getType( - walletPaymentNewSessionToken.success + paymentsGetNewSessionTokenAction.success )} when newSessionToken is 200`, () => { const T_SESSION_TOKEN = "ABCD"; const mocknewSessionToken = jest.fn(); @@ -21,39 +21,39 @@ describe("Test handleWalletPaymentNewSessionToken saga", () => { testSaga( handleWalletPaymentNewSessionToken, mocknewSessionToken, - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next() .call( withRefreshApiCall, mocknewSessionToken(), - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next(E.right({ status: 200, value: newSessionTokenResponse })) - .put(walletPaymentNewSessionToken.success(newSessionTokenResponse)) + .put(paymentsGetNewSessionTokenAction.success(newSessionTokenResponse)) .next() .isDone(); }); it(`should put ${getType( - walletPaymentNewSessionToken.failure + paymentsGetNewSessionTokenAction.failure )} when newSessionToken is not 200`, () => { const mocknewSessionToken = jest.fn(); testSaga( handleWalletPaymentNewSessionToken, mocknewSessionToken, - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next() .call( withRefreshApiCall, mocknewSessionToken(), - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next(E.right({ status: 400, value: undefined })) .put( - walletPaymentNewSessionToken.failure( + paymentsGetNewSessionTokenAction.failure( getGenericError(new Error(`Error: 400`)) ) ) @@ -62,24 +62,24 @@ describe("Test handleWalletPaymentNewSessionToken saga", () => { }); it(`should put ${getType( - walletPaymentNewSessionToken.failure + paymentsGetNewSessionTokenAction.failure )} when newSessionToken encoders returns an error`, () => { const mocknewSessionToken = jest.fn(); testSaga( handleWalletPaymentNewSessionToken, mocknewSessionToken, - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next() .call( withRefreshApiCall, mocknewSessionToken(), - walletPaymentNewSessionToken.request() + paymentsGetNewSessionTokenAction.request() ) .next(E.left([])) .put( - walletPaymentNewSessionToken.failure({ + paymentsGetNewSessionTokenAction.failure({ ...getGenericError(new Error(readablePrivacyReport([]))) }) ) diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentAuthorization.ts similarity index 79% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentAuthorization.ts index d45ea8f6d29..b488f36edee 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentAuthorization.ts @@ -11,20 +11,22 @@ 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 { PaymentClient } from "../../../common/api/client"; +import { paymentsStartPaymentAuthorizationAction } from "../../store/actions/networking"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentAuthorization( requestTransactionAuthorization: PaymentClient["requestTransactionAuthorization"], - action: ActionType<(typeof walletPaymentAuthorization)["request"]> + action: ActionType< + (typeof paymentsStartPaymentAuthorizationAction)["request"] + > ) { try { const sessionToken = yield* getOrFetchWalletSessionToken(); if (sessionToken === undefined) { yield* put( - walletPaymentAuthorization.failure({ + paymentsStartPaymentAuthorizationAction.failure({ ...getGenericError(new Error(`Missing session token`)) }) ); @@ -60,15 +62,15 @@ export function* handleWalletPaymentAuthorization( requestTransactionAuthorizationResult, E.fold( error => - walletPaymentAuthorization.failure({ + paymentsStartPaymentAuthorizationAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), res => { if (res.status === 200) { - return walletPaymentAuthorization.success(res.value); + return paymentsStartPaymentAuthorizationAction.success(res.value); } - return walletPaymentAuthorization.failure({ + return paymentsStartPaymentAuthorizationAction.failure({ ...getGenericError(new Error(`Error: ${res.status}`)) }); } @@ -76,6 +78,8 @@ export function* handleWalletPaymentAuthorization( ) ); } catch (e) { - yield* put(walletPaymentAuthorization.failure({ ...getNetworkError(e) })); + yield* put( + paymentsStartPaymentAuthorizationAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCalculateFees.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts similarity index 71% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentCalculateFees.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts index dc6450a9d6d..a394387dad3 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCalculateFees.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts @@ -11,15 +11,15 @@ import { getGenericError, getNetworkError } from "../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; import { getSortedPspList } from "../../../common/utils"; -import { PaymentClient } from "../../api/client"; -import { walletPaymentCalculateFees } from "../../store/actions/networking"; -import { walletPaymentPickPsp } from "../../store/actions/orchestration"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsCalculatePaymentFeesAction } from "../../store/actions/networking"; +import { selectPaymentPspAction } from "../../store/actions/orchestration"; import { walletPaymentPickedPspSelector } from "../../store/selectors"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentCalculateFees( calculateFees: PaymentClient["calculateFees"], - action: ActionType<(typeof walletPaymentCalculateFees)["request"]> + action: ActionType<(typeof paymentsCalculatePaymentFeesAction)["request"]> ) { try { const preferredLanguageOption = yield* select(preferredLanguageSelector); @@ -33,7 +33,7 @@ export function* handleWalletPaymentCalculateFees( if (sessionToken === undefined) { yield* put( - walletPaymentCalculateFees.failure({ + paymentsCalculatePaymentFeesAction.failure({ ...getGenericError(new Error(`Missing session token`)) }) ); @@ -55,7 +55,7 @@ export function* handleWalletPaymentCalculateFees( if (E.isLeft(calculateFeesResult)) { yield* put( - walletPaymentCalculateFees.failure({ + paymentsCalculatePaymentFeesAction.failure({ ...getGenericError( new Error(readablePrivacyReport(calculateFeesResult.left)) ) @@ -71,23 +71,36 @@ export function* handleWalletPaymentCalculateFees( ); const chosenPsp = yield* select(walletPaymentPickedPspSelector); // If the sorted psp list has the first element marked as "onUs" and the user has not already chosen a psp, we pre-select the first element - if (bundlesSortedByDefault[0]?.onUs && O.isNone(chosenPsp)) { - yield* put(walletPaymentPickPsp(bundlesSortedByDefault[0])); + if ( + (bundlesSortedByDefault[0]?.onUs && O.isNone(chosenPsp)) || + bundlesSortedByDefault.length === 1 + ) { + yield* put(selectPaymentPspAction(bundlesSortedByDefault[0])); + } + if (bundlesSortedByDefault.length === 0) { + yield* put( + paymentsCalculatePaymentFeesAction.failure({ + ...getGenericError(new Error(`Error: The bundles list is empty`)) + }) + ); + return; } const sortedResponse: CalculateFeeResponse = { ...res.value, bundles: res.value.bundles }; - yield* put(walletPaymentCalculateFees.success(sortedResponse)); + yield* put(paymentsCalculatePaymentFeesAction.success(sortedResponse)); return; } yield* put( - walletPaymentCalculateFees.failure({ + paymentsCalculatePaymentFeesAction.failure({ ...getGenericError(new Error(`Error: ${res.status}`)) }) ); } } catch (e) { - yield* put(walletPaymentCalculateFees.failure({ ...getNetworkError(e) })); + yield* put( + paymentsCalculatePaymentFeesAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCreateTransaction.ts similarity index 74% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentCreateTransaction.ts index 770d24100d1..d99fc8a324f 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCreateTransaction.ts @@ -6,20 +6,20 @@ 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 { walletPaymentCreateTransaction } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsCreateTransactionAction } from "../../store/actions/networking"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentCreateTransaction( newTransaction: PaymentClient["newTransaction"], - action: ActionType<(typeof walletPaymentCreateTransaction)["request"]> + action: ActionType<(typeof paymentsCreateTransactionAction)["request"]> ) { try { const sessionToken = yield* getOrFetchWalletSessionToken(); if (sessionToken === undefined) { yield* put( - walletPaymentCreateTransaction.failure({ + paymentsCreateTransactionAction.failure({ ...getGenericError(new Error(`Missing session token`)) }) ); @@ -42,18 +42,18 @@ export function* handleWalletPaymentCreateTransaction( newTransactionResult, E.fold( error => - walletPaymentCreateTransaction.failure({ + paymentsCreateTransactionAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), ({ status, value }) => { if (status === 200) { - return walletPaymentCreateTransaction.success(value); + return paymentsCreateTransactionAction.success(value); } else if (status === 400) { - return walletPaymentCreateTransaction.failure({ + return paymentsCreateTransactionAction.failure({ ...getGenericError(new Error(`Error: ${status}`)) }); } else { - return walletPaymentCreateTransaction.failure(value); + return paymentsCreateTransactionAction.failure(value); } } ) @@ -61,7 +61,7 @@ export function* handleWalletPaymentCreateTransaction( ); } catch (e) { yield* put( - walletPaymentCreateTransaction.failure({ ...getNetworkError(e) }) + paymentsCreateTransactionAction.failure({ ...getNetworkError(e) }) ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentDeleteTransaction.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentDeleteTransaction.ts similarity index 77% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentDeleteTransaction.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentDeleteTransaction.ts index 60448894856..6f49a581021 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentDeleteTransaction.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentDeleteTransaction.ts @@ -6,20 +6,20 @@ 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 { walletPaymentDeleteTransaction } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsDeleteTransactionAction } from "../../store/actions/networking"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentDeleteTransaction( requestTransactionUserCancellation: PaymentClient["requestTransactionUserCancellation"], - action: ActionType<(typeof walletPaymentDeleteTransaction)["request"]> + action: ActionType<(typeof paymentsDeleteTransactionAction)["request"]> ) { try { const sessionToken = yield* getOrFetchWalletSessionToken(); if (sessionToken === undefined) { yield* put( - walletPaymentDeleteTransaction.failure({ + paymentsDeleteTransactionAction.failure({ ...getGenericError(new Error(`Missing session token`)) }) ); @@ -43,15 +43,15 @@ export function* handleWalletPaymentDeleteTransaction( requestTransactionUserCancellationResult, E.fold( error => - walletPaymentDeleteTransaction.failure({ + paymentsDeleteTransactionAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), res => { if (res.status === 202) { - return walletPaymentDeleteTransaction.success(); + return paymentsDeleteTransactionAction.success(); } - return walletPaymentDeleteTransaction.failure({ + return paymentsDeleteTransactionAction.failure({ ...getGenericError(new Error(`Error: ${res.status}`)) }); } @@ -60,7 +60,7 @@ export function* handleWalletPaymentDeleteTransaction( ); } catch (e) { yield* put( - walletPaymentDeleteTransaction.failure({ ...getNetworkError(e) }) + paymentsDeleteTransactionAction.failure({ ...getNetworkError(e) }) ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetAllMethods.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetAllMethods.ts similarity index 52% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetAllMethods.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentGetAllMethods.ts index 59210a00403..aacc0f7d348 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetAllMethods.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetAllMethods.ts @@ -6,14 +6,28 @@ import { SagaCallReturnType } from "../../../../../types/utils"; import { getGenericError, getNetworkError } from "../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; -import { WalletClient } from "../../../common/api/client"; -import { walletPaymentGetAllMethods } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsGetPaymentMethodsAction } from "../../store/actions/networking"; +import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentGetAllMethods( - getAllPaymentMethods: WalletClient["getAllPaymentMethods"], - action: ActionType<(typeof walletPaymentGetAllMethods)["request"]> + getAllPaymentMethods: PaymentClient["getAllPaymentMethods"], + action: ActionType<(typeof paymentsGetPaymentMethodsAction)["request"]> ) { - const getAllPaymentMethodsRequest = getAllPaymentMethods({}); + const sessionToken = yield* getOrFetchWalletSessionToken(); + + if (sessionToken === undefined) { + yield* put( + paymentsGetPaymentMethodsAction.failure({ + ...getGenericError(new Error(`Missing session token`)) + }) + ); + return; + } + + const getAllPaymentMethodsRequest = getAllPaymentMethods({ + eCommerceSessionToken: sessionToken + }); try { const getAllPaymentMethodsResult = (yield* call( @@ -27,15 +41,15 @@ export function* handleWalletPaymentGetAllMethods( getAllPaymentMethodsResult, E.fold( error => - walletPaymentGetAllMethods.failure({ + paymentsGetPaymentMethodsAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), res => { if (res.status === 200) { - return walletPaymentGetAllMethods.success(res.value); + return paymentsGetPaymentMethodsAction.success(res.value); } - return walletPaymentGetAllMethods.failure({ + return paymentsGetPaymentMethodsAction.failure({ ...getGenericError(new Error(`Error: ${res.status}`)) }); } @@ -43,6 +57,8 @@ export function* handleWalletPaymentGetAllMethods( ) ); } catch (e) { - yield* put(walletPaymentGetAllMethods.failure({ ...getNetworkError(e) })); + yield* put( + paymentsGetPaymentMethodsAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetDetails.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetDetails.ts similarity index 73% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetDetails.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentGetDetails.ts index 9b4d35ecb89..7501d53c359 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetDetails.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetDetails.ts @@ -6,20 +6,20 @@ 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 { walletPaymentGetDetails } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsGetPaymentDetailsAction } from "../../store/actions/networking"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentGetDetails( getPaymentRequestInfo: PaymentClient["getPaymentRequestInfo"], - action: ActionType<(typeof walletPaymentGetDetails)["request"]> + action: ActionType<(typeof paymentsGetPaymentDetailsAction)["request"]> ) { try { const sessionToken = yield* getOrFetchWalletSessionToken(); if (sessionToken === undefined) { yield* put( - walletPaymentGetDetails.failure( + paymentsGetPaymentDetailsAction.failure( getGenericError(new Error(`Missing session token`)) ) ); @@ -42,24 +42,26 @@ export function* handleWalletPaymentGetDetails( getPaymentRequestInfoResult, E.fold( error => - walletPaymentGetDetails.failure({ + paymentsGetPaymentDetailsAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), ({ status, value }) => { if (status === 200) { - return walletPaymentGetDetails.success(value); + return paymentsGetPaymentDetailsAction.success(value); } else if (status === 400) { - return walletPaymentGetDetails.failure({ + return paymentsGetPaymentDetailsAction.failure({ ...getGenericError(new Error(`Error: ${status}`)) }); } else { - return walletPaymentGetDetails.failure(value); + return paymentsGetPaymentDetailsAction.failure(value); } } ) ) ); } catch (e) { - yield* put(walletPaymentGetDetails.failure({ ...getNetworkError(e) })); + yield* put( + paymentsGetPaymentDetailsAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetTransactionInfo.ts similarity index 74% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentGetTransactionInfo.ts index be09557b229..792f2562e9f 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetTransactionInfo.ts @@ -6,19 +6,21 @@ 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 { walletPaymentGetTransactionInfo } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsGetPaymentTransactionInfoAction } from "../../store/actions/networking"; import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; export function* handleWalletPaymentGetTransactionInfo( getTransactionInfo: PaymentClient["getTransactionInfo"], - action: ActionType<(typeof walletPaymentGetTransactionInfo)["request"]> + action: ActionType< + (typeof paymentsGetPaymentTransactionInfoAction)["request"] + > ) { const sessionToken = yield* getOrFetchWalletSessionToken(); if (sessionToken === undefined) { yield* put( - walletPaymentGetTransactionInfo.failure({ + paymentsGetPaymentTransactionInfoAction.failure({ ...getGenericError(new Error(`Missing session token`)) }) ); @@ -42,14 +44,14 @@ export function* handleWalletPaymentGetTransactionInfo( getTransactionInfoResult, E.fold( error => - walletPaymentGetTransactionInfo.failure({ + paymentsGetPaymentTransactionInfoAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), ({ status, value }) => { if (status === 200) { - return walletPaymentGetTransactionInfo.success(value); + return paymentsGetPaymentTransactionInfoAction.success(value); } else { - return walletPaymentGetTransactionInfo.failure({ + return paymentsGetPaymentTransactionInfoAction.failure({ ...getGenericError(new Error(JSON.stringify(value))) }); } @@ -59,7 +61,7 @@ export function* handleWalletPaymentGetTransactionInfo( ); } catch (e) { yield* put( - walletPaymentGetTransactionInfo.failure({ ...getNetworkError(e) }) + paymentsGetPaymentTransactionInfoAction.failure({ ...getNetworkError(e) }) ); } } diff --git a/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetUserWallets.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetUserWallets.ts new file mode 100644 index 00000000000..b4c58411096 --- /dev/null +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentGetUserWallets.ts @@ -0,0 +1,68 @@ +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 { SagaCallReturnType } from "../../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../../utils/reporters"; +import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsGetPaymentUserMethodsAction } from "../../store/actions/networking"; +import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; + +export function* handleWalletPaymentGetUserWallets( + getWalletsByIdUser: PaymentClient["getWalletsByIdUser"], + action: ActionType<(typeof paymentsGetPaymentUserMethodsAction)["request"]> +) { + const sessionToken = yield* getOrFetchWalletSessionToken(); + + if (sessionToken === undefined) { + yield* put( + paymentsGetPaymentUserMethodsAction.failure({ + ...getGenericError(new Error(`Missing session token`)) + }) + ); + return; + } + + const getWalletsByIdUserRequest = getWalletsByIdUser({ + eCommerceSessionToken: sessionToken + }); + + try { + const getWalletsByIdUserResult = (yield* call( + withRefreshApiCall, + getWalletsByIdUserRequest, + action + )) as SagaCallReturnType; + + yield* put( + pipe( + getWalletsByIdUserResult, + E.fold( + error => + paymentsGetPaymentUserMethodsAction.failure( + getGenericError(new Error(readablePrivacyReport(error))) + ), + res => { + if (res.status === 200) { + return paymentsGetPaymentUserMethodsAction.success(res.value); + } + if (res.status === 404) { + return paymentsGetPaymentUserMethodsAction.success({ + wallets: [] + }); + } + return paymentsGetPaymentUserMethodsAction.failure({ + ...getGenericError(new Error(`Error: ${res.status}`)) + }); + } + ) + ) + ); + } catch (e) { + yield* put( + paymentsGetPaymentUserMethodsAction.failure({ ...getNetworkError(e) }) + ); + } +} diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentNewSessionToken.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentNewSessionToken.ts similarity index 78% rename from ts/features/walletV3/payment/saga/networking/handleWalletPaymentNewSessionToken.ts rename to ts/features/payments/checkout/saga/networking/handleWalletPaymentNewSessionToken.ts index e3f8e1ac084..32119d66b68 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentNewSessionToken.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentNewSessionToken.ts @@ -6,8 +6,8 @@ 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 { walletPaymentNewSessionToken } from "../../store/actions/networking"; +import { PaymentClient } from "../../../common/api/client"; +import { paymentsGetNewSessionTokenAction } from "../../store/actions/networking"; import { selectWalletPaymentSessionToken } from "../../store/selectors"; /** @@ -30,12 +30,12 @@ export function* getOrFetchWalletSessionToken(timeoutMs: number = 3000) { } // If the session token is not present, dispatch a new request action - yield* put(walletPaymentNewSessionToken.request()); + yield* put(paymentsGetNewSessionTokenAction.request()); // Wait for the request to end, either in success or failure, with a timeout const { data } = yield* race({ - data: take(walletPaymentNewSessionToken.success), - failure: take(walletPaymentNewSessionToken.failure), + data: take(paymentsGetNewSessionTokenAction.success), + failure: take(paymentsGetNewSessionTokenAction.failure), timeout: delay(timeoutMs) }); @@ -45,7 +45,7 @@ export function* getOrFetchWalletSessionToken(timeoutMs: number = 3000) { export function* handleWalletPaymentNewSessionToken( newSessionToken: PaymentClient["newSessionToken"], - action: ActionType<(typeof walletPaymentNewSessionToken)["request"]> + action: ActionType<(typeof paymentsGetNewSessionTokenAction)["request"]> ) { const newSessionTokenRequest = newSessionToken({}); @@ -61,14 +61,14 @@ export function* handleWalletPaymentNewSessionToken( newSessionTokenResult, E.fold( error => - walletPaymentNewSessionToken.failure({ + paymentsGetNewSessionTokenAction.failure({ ...getGenericError(new Error(readablePrivacyReport(error))) }), ({ status, value }) => { if (status === 200) { - return walletPaymentNewSessionToken.success(value); + return paymentsGetNewSessionTokenAction.success(value); } else { - return walletPaymentNewSessionToken.failure({ + return paymentsGetNewSessionTokenAction.failure({ ...getGenericError(new Error(`Error: ${status}`)) }); } @@ -77,6 +77,8 @@ export function* handleWalletPaymentNewSessionToken( ) ); } catch (e) { - yield* put(walletPaymentNewSessionToken.failure({ ...getNetworkError(e) })); + yield* put( + paymentsGetNewSessionTokenAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentConfirmScreen.tsx similarity index 81% rename from ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentConfirmScreen.tsx index 7306b4f6750..6deea893463 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentConfirmScreen.tsx @@ -5,24 +5,18 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useNavigation } from "@react-navigation/native"; import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { SafeAreaView, StyleSheet } from "react-native"; import { AmountEuroCents } from "../../../../../definitions/pagopa/ecommerce/AmountEuroCents"; -import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../../store/hooks"; -import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { WalletPaymentConfirmContent } from "../components/WalletPaymentConfirmContent"; import { useWalletPaymentAuthorizationModal } from "../hooks/useWalletPaymentAuthorizationModal"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; import { walletPaymentDetailsSelector, walletPaymentPickedPaymentMethodSelector, @@ -35,7 +29,7 @@ import { } from "../types/PaymentOutcomeEnum"; const WalletPaymentConfirmScreen = () => { - const navigation = useNavigation>(); + const navigation = useIONavigation(); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); const transactionPot = useIOSelector(walletPaymentTransactionSelector); @@ -44,13 +38,6 @@ const WalletPaymentConfirmScreen = () => { ); const selectedPspOption = useIOSelector(walletPaymentPickedPspSelector); - useHeaderSecondLevel({ - title: "", - contextualHelp: emptyContextualHelp, - faqCategories: ["payment"], - supportRequest: true - }); - const handleStartPaymentAuthorization = () => pipe( sequenceS(O.Monad)({ @@ -72,8 +59,8 @@ const WalletPaymentConfirmScreen = () => { const handleAuthorizationOutcome = React.useCallback( (outcome: WalletPaymentOutcome) => { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME, + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, params: { outcome } @@ -88,9 +75,7 @@ const WalletPaymentConfirmScreen = () => { isPendingAuthorization, startPaymentAuthorizaton } = useWalletPaymentAuthorizationModal({ - onAuthorizationOutcome: handleAuthorizationOutcome, - onDismiss: () => - handleAuthorizationOutcome(WalletPaymentOutcomeEnum.CANCELED_BY_USER) + onAuthorizationOutcome: handleAuthorizationOutcome }); const isLoading = isAuthUrlLoading || isPendingAuthorization; diff --git a/ts/features/walletV3/payment/screens/WalletPaymentDetailScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentDetailScreen.tsx similarity index 90% rename from ts/features/walletV3/payment/screens/WalletPaymentDetailScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentDetailScreen.tsx index 7969f2c538d..cd6a435a15a 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentDetailScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentDetailScreen.tsx @@ -46,19 +46,20 @@ import { formatNumberAmount } from "../../../../utils/stringBuilder"; import { WalletPaymentFailureDetail } from "../components/WalletPaymentFailureDetail"; -import { WalletPaymentParamsList } from "../navigation/params"; -import { WalletPaymentRoutes } from "../navigation/routes"; -import { walletPaymentGetDetails } from "../store/actions/networking"; +import { PaymentsCheckoutParamsList } from "../navigation/params"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; +import { paymentsGetPaymentDetailsAction } from "../store/actions/networking"; import { walletPaymentDetailsSelector } from "../store/selectors"; -import { WalletPaymentFailure } from "../types/failure"; +import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; +import { storeNewPaymentAttemptAction } from "../../history/store/actions"; type WalletPaymentDetailScreenNavigationParams = { rptId: RptId; }; type WalletPaymentDetailRouteProps = RouteProp< - WalletPaymentParamsList, - "WALLET_PAYMENT_DETAIL" + PaymentsCheckoutParamsList, + "PAYMENT_CHECKOUT_DETAIL" >; const WalletPaymentDetailScreen = () => { @@ -70,7 +71,7 @@ const WalletPaymentDetailScreen = () => { useFocusEffect( React.useCallback(() => { - dispatch(walletPaymentGetDetails.request(rptId)); + dispatch(paymentsGetPaymentDetailsAction.request(rptId)); }, [dispatch, rptId]) ); @@ -117,6 +118,7 @@ const WalletPaymentDetailContent = ({ rptId, payment }: WalletPaymentDetailContentProps) => { + const dispatch = useIODispatch(); const navigation = useNavigation>(); useLayoutEffect(() => { @@ -131,9 +133,10 @@ const WalletPaymentDetailContent = ({ contextualHelp: emptyContextualHelp }); - const navigateToMethodSelection = () => { - navigation.push(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_METHOD + const navigateToMakePaymentScreen = () => { + dispatch(storeNewPaymentAttemptAction(rptId)); + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_MAKE }); }; @@ -157,7 +160,7 @@ const WalletPaymentDetailContent = ({ ); const amount = pipe(payment.amount, centsToAmount, amount => - formatNumberAmount(amount, true) + formatNumberAmount(amount, true, "right") ); const dueDate = pipe( @@ -200,7 +203,7 @@ const WalletPaymentDetailContent = ({ primaryActionProps={{ label: "Vai al pagamento", accessibilityLabel: "Vai al pagmento", - onPress: navigateToMethodSelection + onPress: navigateToMakePaymentScreen }} > ; }; type WalletPaymentInputFiscalCodeRouteProps = RouteProp< - WalletPaymentParamsList, - "WALLET_PAYMENT_INPUT_FISCAL_CODE" + PaymentsCheckoutParamsList, + "PAYMENT_CHECKOUT_INPUT_FISCAL_CODE" >; type InputState = { @@ -59,11 +63,23 @@ const WalletPaymentInputFiscalCodeScreen = () => { const { params } = useRoute(); const dispatch = useIODispatch(); const navigation = useNavigation>(); + + const { startPaymentFlowWithRptId, isNewWalletSectionEnabled } = + usePagoPaPayment(); + const [inputState, setInputState] = React.useState({ fiscalCodeText: "", fiscalCode: O.none }); + const textInputWrappperRef = React.useRef(null); + const focusTextInput = () => { + const textInputA11yWrapper = findNodeHandle(textInputWrappperRef.current); + if (textInputA11yWrapper) { + AccessibilityInfo.setAccessibilityFocus(textInputA11yWrapper); + } + }; + const navigateToTransactionSummary = () => { pipe( sequenceS(O.Monad)({ @@ -72,20 +88,37 @@ const WalletPaymentInputFiscalCodeScreen = () => { }), O.chain(flow(RptId.decode, O.fromEither)), O.map(rptId => { - dispatch(paymentInitializeState()); - navigation.navigate(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, - params: { - // Set the initial amount to a fixed value (1) because it is not used - initialAmount: "1" as AmountInEuroCents, - rptId, - paymentStartOrigin: "manual_insertion" - } - }); + // Removes the manual input screen from the stack + navigation.popToTop(); + navigation.pop(); + // Navigate to the payment details screen (payment verification) + if (isNewWalletSectionEnabled) { + startPaymentFlowWithRptId(rptId); + } else { + dispatch(paymentInitializeState()); + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, + params: { + // Set the initial amount to a fixed value (1) because it is not used + initialAmount: "1" as AmountInEuroCents, + rptId, + paymentStartOrigin: "manual_insertion" + } + }); + } }) ); }; + const handleContinueClick = () => + pipe( + inputState.fiscalCode, + O.fold(() => { + Keyboard.dismiss(); + focusTextInput(); + }, navigateToTransactionSummary) + ); + return ( @@ -95,29 +128,32 @@ const WalletPaymentInputFiscalCodeScreen = () => { {I18n.t("wallet.payment.manual.fiscalCode.subtitle")} - - setInputState({ - fiscalCodeText: value, - fiscalCode: decodeOrganizationFiscalCode(value) - }) - } - onValidate={validateOrganizationFiscalCode} - counterLimit={11} - textInputProps={{ - keyboardType: "number-pad", - inputMode: "numeric", - returnKeyType: "done" - }} - /> + + + setInputState({ + fiscalCodeText: value, + fiscalCode: decodeOrganizationFiscalCode(value) + }) + } + onValidate={validateOrganizationFiscalCode} + counterLimit={11} + textInputProps={{ + keyboardType: "number-pad", + inputMode: "numeric", + returnKeyType: "done" + }} + autoFocus + /> +
{
diff --git a/ts/features/walletV3/payment/screens/WalletPaymentInputNoticeNumberScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentInputNoticeNumberScreen.tsx similarity index 57% rename from ts/features/walletV3/payment/screens/WalletPaymentInputNoticeNumberScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentInputNoticeNumberScreen.tsx index 41b1b664331..7d9e108a62b 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentInputNoticeNumberScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentInputNoticeNumberScreen.tsx @@ -10,12 +10,16 @@ import { import { PaymentNoticeNumberFromString } from "@pagopa/io-pagopa-commons/lib/pagopa"; import { useNavigation } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import React from "react"; import { + AccessibilityInfo, + Keyboard, KeyboardAvoidingView, Platform, SafeAreaView, - View + View, + findNodeHandle } from "react-native"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import { @@ -28,7 +32,7 @@ import { decodePaymentNoticeNumber, validatePaymentNoticeNumber } from "../../common/utils/validation"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; import I18n from "../../../../i18n"; type InputState = { @@ -44,14 +48,32 @@ const WalletPaymentInputNoticeNumberScreen = () => { }); const navigateToFiscalCodeInput = () => { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_INPUT_FISCAL_CODE, + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_INPUT_FISCAL_CODE, params: { paymentNoticeNumber: inputState.noticeNumber } }); }; + const handleContinueClick = () => + pipe( + inputState.noticeNumber, + O.fold(() => { + Keyboard.dismiss(); + focusTextInput(); + }, navigateToFiscalCodeInput) + ); + + const focusTextInput = () => { + const textInputA11yWrapper = findNodeHandle(textInputWrappperRef.current); + if (textInputA11yWrapper && O.isNone(inputState.noticeNumber)) { + AccessibilityInfo.setAccessibilityFocus(textInputA11yWrapper); + } + }; + + const textInputWrappperRef = React.useRef(null); + return ( @@ -61,29 +83,32 @@ const WalletPaymentInputNoticeNumberScreen = () => { {I18n.t("wallet.payment.manual.noticeNumber.subtitle")} - - setInputState({ - noticeNumberText: value, - noticeNumber: decodePaymentNoticeNumber(value) - }) - } - onValidate={validatePaymentNoticeNumber} - counterLimit={18} - textInputProps={{ - keyboardType: "number-pad", - inputMode: "numeric", - returnKeyType: "done" - }} - /> + + + setInputState({ + noticeNumberText: value, + noticeNumber: decodePaymentNoticeNumber(value) + }) + } + onValidate={validatePaymentNoticeNumber} + counterLimit={18} + textInputProps={{ + keyboardType: "number-pad", + inputMode: "numeric", + returnKeyType: "done" + }} + autoFocus + /> +
{ diff --git a/ts/features/payments/checkout/screens/WalletPaymentMakeScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentMakeScreen.tsx new file mode 100644 index 00000000000..e3ca4ab3ee0 --- /dev/null +++ b/ts/features/payments/checkout/screens/WalletPaymentMakeScreen.tsx @@ -0,0 +1,49 @@ +import { IOStyles } from "@pagopa/io-app-design-system"; +import React, { useLayoutEffect } from "react"; +import { View } from "react-native"; +import PagerView from "react-native-pager-view"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIOSelector } from "../../../../store/hooks"; +import { WalletPaymentHeader } from "../components/WalletPaymentHeader"; +import { selectWalletPaymentCurrentStep } from "../store/selectors"; +import { WalletPaymentConfirmScreen } from "./WalletPaymentConfirmScreen"; +import { WalletPaymentPickMethodScreen } from "./WalletPaymentPickMethodScreen"; +import { WalletPaymentPickPspScreen } from "./WalletPaymentPickPspScreen"; + +const WalletPaymentMakeScreen = () => { + const navigation = useIONavigation(); + const ref = React.useRef(null); + const currentStep = useIOSelector(selectWalletPaymentCurrentStep); + + React.useEffect(() => { + ref.current?.setPage(currentStep - 1); + }, [ref, currentStep]); + + useLayoutEffect(() => { + navigation.setOptions({ + header: () => , + headerShown: true + }); + }, [navigation, currentStep]); + + return ( + + + + + + + + + + + + ); +}; + +export { WalletPaymentMakeScreen }; diff --git a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentOutcomeScreen.tsx similarity index 76% rename from ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentOutcomeScreen.tsx index 2019e066ea5..6f82e4141dc 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentOutcomeScreen.tsx @@ -1,5 +1,5 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; -import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; +import { RouteProp, useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; @@ -8,32 +8,31 @@ import { OperationResultScreenContentProps } from "../../../../components/screens/OperationResultScreenContent"; import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../../store/hooks"; +import { profileEmailSelector } from "../../../../store/reducers/profile"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { useAvoidHardwareBackButton } from "../../../../utils/useAvoidHardwareBackButton"; import { WalletPaymentFeebackBanner } from "../components/WalletPaymentFeedbackBanner"; import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; -import { WalletPaymentParamsList } from "../navigation/params"; +import { PaymentsCheckoutParamsList } from "../navigation/params"; import { walletPaymentDetailsSelector, - walletPaymentStartRouteSelector + walletPaymentOnSuccessActionSelector } from "../store/selectors"; import { WalletPaymentOutcome, WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; -import { useAvoidHardwareBackButton } from "../../../../utils/useAvoidHardwareBackButton"; +import ROUTES from "../../../../navigation/routes"; type WalletPaymentOutcomeScreenNavigationParams = { outcome: WalletPaymentOutcome; }; type WalletPaymentOutcomeRouteProps = RouteProp< - WalletPaymentParamsList, - "WALLET_PAYMENT_OUTCOME" + PaymentsCheckoutParamsList, + "PAYMENT_CHECKOUT_OUTCOME" >; const WalletPaymentOutcomeScreen = () => { @@ -42,9 +41,10 @@ const WalletPaymentOutcomeScreen = () => { const { params } = useRoute(); const { outcome } = params; - const navigation = useNavigation>(); + const navigation = useIONavigation(); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); - const paymentStartRoute = useIOSelector(walletPaymentStartRouteSelector); + const onSuccessAction = useIOSelector(walletPaymentOnSuccessActionSelector); + const profileEmailOption = useIOSelector(profileEmailSelector); const supportModal = usePaymentFailureSupportModal({ outcome @@ -74,13 +74,19 @@ const WalletPaymentOutcomeScreen = () => { }; const handleClose = () => { - if (paymentStartRoute) { - navigation.navigate(paymentStartRoute.routeName, { - screen: paymentStartRoute.routeKey + if ( + onSuccessAction === "showHome" || + onSuccessAction === "showTransaction" + ) { + // Currently we do support only navigation to the wallet + // TODO navigate to the transaction details if payment outcome is success + navigation.popToTop(); + navigation.navigate(ROUTES.MAIN, { + screen: ROUTES.PAYMENTS_HOME }); return; } - navigation.popToTop(); + navigation.pop(); }; @@ -183,6 +189,37 @@ const WalletPaymentOutcomeScreen = () => { action: contactSupportAction, secondaryAction: closeFailureAction }; + case WalletPaymentOutcomeEnum.WAITING_CONFIRMATION_EMAIL: + return { + pictogram: "timing", + title: I18n.t( + "wallet.payment.outcome.WAITING_CONFIRMATION_EMAIL.title" + ), + subtitle: pipe( + profileEmailOption, + O.map(email => + I18n.t( + "wallet.payment.outcome.WAITING_CONFIRMATION_EMAIL.subtitle", + { email } + ) + ), + O.getOrElse(() => + I18n.t( + "wallet.payment.outcome.WAITING_CONFIRMATION_EMAIL.defaultSubtitle" + ) + ) + ), + action: closeFailureAction + }; + case WalletPaymentOutcomeEnum.METHOD_NOT_ENABLED: + return { + pictogram: "activate", + title: I18n.t("wallet.payment.outcome.METHOD_NOT_ENABLED.title"), + subtitle: I18n.t( + "wallet.payment.outcome.METHOD_NOT_ENABLED.subtitle" + ), + action: closeFailureAction + }; } }; diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx similarity index 73% rename from ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx index 74c73aab212..2bc9183b7b7 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx @@ -10,7 +10,7 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useFocusEffect, useNavigation } from "@react-navigation/native"; +import { useFocusEffect } from "@react-navigation/native"; import { sequenceS, sequenceT } from "fp-ts/lib/Apply"; import * as A from "fp-ts/lib/Array"; import * as O from "fp-ts/lib/Option"; @@ -18,37 +18,40 @@ import { pipe } from "fp-ts/lib/function"; import { capitalize } from "lodash"; import React, { useEffect, useMemo } from "react"; import { View } from "react-native"; +import { Transfer } from "../../../../../definitions/pagopa/ecommerce/Transfer"; +import { WalletInfo } from "../../../../../definitions/pagopa/ecommerce/WalletInfo"; import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { ComponentProps } from "../../../../types/react"; -import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { findFirstCaseInsensitive } from "../../../../utils/object"; -import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; +import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; import { WalletPaymentMissingMethodsError } from "../components/WalletPaymentMissingMethodsError"; -import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler"; import { useOnTransactionActivationEffect } from "../hooks/useOnTransactionActivationEffect"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; import { - walletPaymentCreateTransaction, - walletPaymentGetUserWallets + paymentsCalculatePaymentFeesAction, + paymentsCreateTransactionAction, + paymentsGetPaymentUserMethodsAction, + paymentsResetPaymentPspList } from "../store/actions/networking"; -import { walletPaymentPickPaymentMethod } from "../store/actions/orchestration"; +import { + selectPaymentMethodAction, + walletPaymentSetCurrentStep +} from "../store/actions/orchestration"; import { walletPaymentAllMethodsSelector, walletPaymentAmountSelector, walletPaymentDetailsSelector, + walletPaymentPickedPaymentMethodSelector, + walletPaymentPspListSelector, walletPaymentSavedMethodByIdSelector, walletPaymentTransactionSelector, walletPaymentUserWalletsSelector } from "../store/selectors"; import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; +import { WalletPaymentStepEnum } from "../types"; type SavedMethodState = { kind: "saved"; @@ -66,17 +69,7 @@ type SelectedMethodState = SavedMethodState | NotSavedMethodState | undefined; const WalletPaymentPickMethodScreen = () => { const dispatch = useIODispatch(); - const navigation = useNavigation>(); - const handleGoBack = useWalletPaymentGoBackHandler(); - - useHeaderSecondLevel({ - title: "", - backAccessibilityLabel: I18n.t("global.buttons.back"), - goBack: handleGoBack, - contextualHelp: emptyContextualHelp, - faqCategories: ["payment"], - supportRequest: true - }); + const navigation = useIONavigation(); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); const getSavedtMethodById = useIOSelector( @@ -86,6 +79,10 @@ const WalletPaymentPickMethodScreen = () => { const paymentMethodsPot = useIOSelector(walletPaymentAllMethodsSelector); const userWalletsPots = useIOSelector(walletPaymentUserWalletsSelector); const transactionPot = useIOSelector(walletPaymentTransactionSelector); + const selectedWalletOption = useIOSelector( + walletPaymentPickedPaymentMethodSelector + ); + const pspListPot = useIOSelector(walletPaymentPspListSelector); // const getGenericMethodById = useIOSelector(walletPaymentGenericMethodByIdSelector); const [waitingTransactionActivation, setWaitingTransactionActivation] = @@ -95,24 +92,66 @@ const WalletPaymentPickMethodScreen = () => { // only when the transaction status becomes ACTIVATED. useOnTransactionActivationEffect( React.useCallback(() => { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP - }); + pipe( + sequenceT(O.Monad)( + pot.toOption(paymentAmountPot), + pot.toOption(transactionPot), + selectedWalletOption + ), + O.map(([paymentAmountInCents, transaction, selectedWallet]) => { + const transferList = transaction.payments.reduce( + (a, p) => [...a, ...(p.transferList ?? [])], + [] as ReadonlyArray + ); + const paymentToken = transaction.payments[0]?.paymentToken; + + dispatch( + paymentsCalculatePaymentFeesAction.request({ + paymentToken, + paymentMethodId: selectedWallet.paymentMethodId, + walletId: selectedWallet.walletId, + paymentAmount: paymentAmountInCents, + transferList + }) + ); + }) + ); setWaitingTransactionActivation(false); - }, [navigation]) + }, [dispatch, paymentAmountPot, selectedWalletOption, transactionPot]) ); + React.useEffect(() => { + pipe( + pspListPot, + pot.toOption, + O.map(pspList => { + if (pspList.length > 1) { + dispatch(walletPaymentSetCurrentStep(WalletPaymentStepEnum.PICK_PSP)); + } else if (pspList.length >= 1) { + dispatch( + walletPaymentSetCurrentStep( + WalletPaymentStepEnum.CONFIRM_TRANSACTION + ) + ); + } + }) + ); + }, [pspListPot, dispatch]); + const alertRef = React.useRef(null); const isLoading = pot.isLoading(paymentMethodsPot) || pot.isLoading(userWalletsPots); const isLoadingTransaction = - pot.isLoading(transactionPot) || waitingTransactionActivation; + pot.isLoading(transactionPot) || + waitingTransactionActivation || + pot.isLoading(pspListPot); const isError = pot.isError(transactionPot) || pot.isError(paymentMethodsPot) || - pot.isError(userWalletsPots); + pot.isError(userWalletsPots) || + pot.isError(pspListPot); const [shouldShowWarningBanner, setShouldShowWarningBanner] = React.useState(false); @@ -123,14 +162,15 @@ const WalletPaymentPickMethodScreen = () => { React.useCallback(() => { // currently we do not allow onboarding new methods in payment flow // dispatch(walletPaymentGetAllMethods.request()); - dispatch(walletPaymentGetUserWallets.request()); + dispatch(paymentsGetPaymentUserMethodsAction.request()); + dispatch(paymentsResetPaymentPspList()); }, [dispatch]) ); React.useEffect(() => { if (isError) { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME, + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, params: { outcome: WalletPaymentOutcomeEnum.GENERIC_ERROR } @@ -184,9 +224,9 @@ const WalletPaymentPickMethodScreen = () => { pot.toOption(paymentDetailsPot) ), O.map(([method, paymentDetails]) => { - dispatch(walletPaymentPickPaymentMethod(method)); + dispatch(selectPaymentMethodAction(method)); dispatch( - walletPaymentCreateTransaction.request({ + paymentsCreateTransactionAction.request({ paymentNotices: [ { rptId: paymentDetails.rptId, amount: paymentDetails.amount } ] @@ -208,7 +248,7 @@ const WalletPaymentPickMethodScreen = () => { } }, [isLoading, genericMethodsListItems, savedMethodsListItems]); - if (!isLoading && savedMethodsListItems.length === 0) { + if (pot.isSome(userWalletsPots) && userWalletsPots.value.length === 0) { return ; } @@ -300,23 +340,29 @@ const mapSavedToRadioItem = ( ): RadioItem | undefined => { const details = method.details as UIWalletInfoDetails; - if (details.maskedPan !== undefined) { + if (details.lastFourDigits !== undefined) { return { id: method.walletId, - value: `${capitalize(details.brand)} ••${details.maskedPan}`, - startImage: getIconWithFallback(details.brand) + value: `${capitalize(details.brand)} ••${details.lastFourDigits}`, + startImage: { + uri: method.paymentMethodAsset + } }; } else if (details.maskedEmail !== undefined) { return { id: method.walletId, value: "PayPal", - startImage: getIconWithFallback("paypal") + startImage: { + uri: method.paymentMethodAsset + } }; } else if (details.maskedNumber !== undefined) { return { id: method.walletId, value: "BANCOMAT Pay", - startImage: getIconWithFallback("bancomatpay") + startImage: { + uri: method.paymentMethodAsset + } }; } diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx similarity index 66% rename from ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx rename to ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx index a6802e37265..7d643d5e237 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx @@ -8,53 +8,38 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useFocusEffect, useNavigation } from "@react-navigation/native"; -import { sequenceT } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; -import { Transfer } from "../../../../../definitions/pagopa/ecommerce/Transfer"; -import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { getSortedPspList } from "../../common/utils"; import { WalletPspListSkeleton } from "../components/WalletPspListSkeleton"; import { useSortPspBottomSheet } from "../hooks/useSortPspBottomSheet"; -import { WalletPaymentRoutes } from "../navigation/routes"; -import { walletPaymentCalculateFees } from "../store/actions/networking"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; import { - walletPaymentPickPsp, - walletPaymentResetPickedPsp + selectPaymentPspAction, + resetPaymentPspAction, + walletPaymentSetCurrentStep } from "../store/actions/orchestration"; import { - walletPaymentAmountSelector, - walletPaymentPickedPaymentMethodSelector, walletPaymentPickedPspSelector, - walletPaymentPspListSelector, - walletPaymentTransactionSelector + walletPaymentPspListSelector } from "../store/selectors"; -import { WalletPaymentPspSortType } from "../types"; +import { WalletPaymentPspSortType, WalletPaymentStepEnum } from "../types"; import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; const WalletPaymentPickPspScreen = () => { - const paymentAmountPot = useIOSelector(walletPaymentAmountSelector); - const selectedWalletOption = useIOSelector( - walletPaymentPickedPaymentMethodSelector - ); const dispatch = useIODispatch(); - const navigation = useNavigation>(); + const navigation = useIONavigation(); + const [showFeaturedPsp, setShowFeaturedPsp] = React.useState(true); const [sortType, setSortType] = React.useState("default"); - const transactionPot = useIOSelector(walletPaymentTransactionSelector); const pspListPot = useIOSelector(walletPaymentPspListSelector); const selectedPspOption = useIOSelector(walletPaymentPickedPspSelector); @@ -71,8 +56,8 @@ const WalletPaymentPickPspScreen = () => { React.useEffect(() => { if (isError) { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME, + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, params: { outcome: WalletPaymentOutcomeEnum.GENERIC_ERROR } @@ -80,45 +65,9 @@ const WalletPaymentPickPspScreen = () => { } }, [isError, navigation]); - useHeaderSecondLevel({ - title: "", - contextualHelp: emptyContextualHelp, - faqCategories: ["payment"], - supportRequest: true - }); - - useFocusEffect( - React.useCallback(() => { - pipe( - sequenceT(O.Monad)( - pot.toOption(paymentAmountPot), - pot.toOption(transactionPot), - selectedWalletOption - ), - O.map(([paymentAmountInCents, transaction, selectedWallet]) => { - const transferList = transaction.payments.reduce( - (a, p) => [...a, ...(p.transferList ?? [])], - [] as ReadonlyArray - ); - const paymentToken = transaction.payments[0]?.paymentToken; - - dispatch( - walletPaymentCalculateFees.request({ - paymentToken, - paymentMethodId: selectedWallet.paymentMethodId, - walletId: selectedWallet.walletId, - paymentAmount: paymentAmountInCents, - transferList - }) - ); - }) - ); - }, [dispatch, paymentAmountPot, selectedWalletOption, transactionPot]) - ); - React.useEffect( () => () => { - dispatch(walletPaymentResetPickedPsp()); + dispatch(resetPaymentPspAction()); }, [dispatch] ); @@ -147,16 +96,16 @@ const WalletPaymentPickPspScreen = () => { ); if (selectedBundle) { - dispatch(walletPaymentPickPsp(selectedBundle)); + dispatch(selectPaymentPspAction(selectedBundle)); } }, [dispatch, sortedPspList] ); const handleContinue = () => { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_CONFIRM - }); + dispatch( + walletPaymentSetCurrentStep(WalletPaymentStepEnum.CONFIRM_TRANSACTION) + ); }; const sortButtonProps: ListItemHeader["endElement"] = React.useMemo( diff --git a/ts/features/payments/checkout/store/__tests__/store.test.ts b/ts/features/payments/checkout/store/__tests__/store.test.ts new file mode 100644 index 00000000000..c299b9c6f5c --- /dev/null +++ b/ts/features/payments/checkout/store/__tests__/store.test.ts @@ -0,0 +1,40 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { walletPaymentSetCurrentStep } from "../actions/orchestration"; +import { PaymentsCheckoutState } from "../reducers"; + +const INITIAL_STATE: PaymentsCheckoutState = { + currentStep: 1, + sessionToken: pot.none, + paymentDetails: pot.none, + userWallets: pot.none, + allPaymentMethods: pot.none, + pspList: pot.none, + chosenPaymentMethod: O.none, + chosenPsp: O.none, + transaction: pot.none, + authorizationUrl: pot.none +}; + +describe("Test Payment reducer", () => { + it("should have initial state at startup", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + expect(globalState.features.payments.checkout).toStrictEqual(INITIAL_STATE); + }); + + it("should correctly update payment step, also when trying to overflow the steps, it should set the steps to WALLET_PAYMENT_STEP_MAX, and in case zero is passed it should set the step to 1", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + expect(globalState.features.payments.checkout).toStrictEqual(INITIAL_STATE); + + const store = createStore(appReducer, globalState as any); + + store.dispatch(walletPaymentSetCurrentStep(2)); + expect(store.getState().features.payments.checkout.currentStep).toBe(2); + + store.dispatch(walletPaymentSetCurrentStep(0)); + expect(store.getState().features.payments.checkout.currentStep).toBe(1); + }); +}); diff --git a/ts/features/payments/checkout/store/actions/index.ts b/ts/features/payments/checkout/store/actions/index.ts new file mode 100644 index 00000000000..beee5bf4333 --- /dev/null +++ b/ts/features/payments/checkout/store/actions/index.ts @@ -0,0 +1,6 @@ +import { PaymentsCheckoutNetworkingActions } from "./networking"; +import { PaymentsCheckoutOrchestrationActions } from "./orchestration"; + +export type PaymentsCheckoutActions = + | PaymentsCheckoutNetworkingActions + | PaymentsCheckoutOrchestrationActions; diff --git a/ts/features/payments/checkout/store/actions/networking.ts b/ts/features/payments/checkout/store/actions/networking.ts new file mode 100644 index 00000000000..a12ae9596d9 --- /dev/null +++ b/ts/features/payments/checkout/store/actions/networking.ts @@ -0,0 +1,111 @@ +import { + ActionType, + createAsyncAction, + createStandardAction +} from "typesafe-actions"; +import { AmountEuroCents } from "../../../../../../definitions/pagopa/ecommerce/AmountEuroCents"; +import { CalculateFeeRequest } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeRequest"; +import { CalculateFeeResponse } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeResponse"; +import { NewSessionTokenResponse } from "../../../../../../definitions/pagopa/ecommerce/NewSessionTokenResponse"; +import { NewTransactionRequest } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionRequest"; +import { NewTransactionResponse } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; +import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; +import { RequestAuthorizationResponse } from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationResponse"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; +import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets"; +import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentMethodsResponse"; +import { NetworkError } from "../../../../../utils/errors"; +import { WalletPaymentFailure } from "../../types/WalletPaymentFailure"; + +export const paymentsGetNewSessionTokenAction = createAsyncAction( + "PAYMENTS_GET_NEW_SESSION_TOKEN_REQUEST", + "PAYMENTS_GET_NEW_SESSION_TOKEN_SUCCESS", + "PAYMENTS_GET_NEW_SESSION_TOKEN_FAILURE" +)(); + +export const paymentsGetPaymentDetailsAction = createAsyncAction( + "PAYMENTS_GET_PAYMENT_DETAILS_REQUEST", + "PAYMENTS_GET_PAYMENT_DETAILS_SUCCESS", + "PAYMENTS_GET_PAYMENT_DETAILS_FAILURE" +)(); + +export const paymentsGetPaymentMethodsAction = createAsyncAction( + "PAYMENTS_GET_PAYMENT_METHODS_REQUEST", + "PAYMENTS_GET_PAYMENT_METHODS_SUCCESS", + "PAYMENTS_GET_PAYMENT_METHODS_FAILURE" +)(); + +export const paymentsGetPaymentUserMethodsAction = createAsyncAction( + "PAYMENTS_GET_PAYMENT_USER_METHODS_REQUEST", + "PAYMENTS_GET_PAYMENT_USER_METHODS_SUCCESS", + "PAYMENTS_GET_PAYMENT_USER_METHODS_FAILURE" +)(); + +export const paymentsCalculatePaymentFeesAction = createAsyncAction( + "PAYMENTS_CALCULATE_PAYMENT_FEES_REQUEST", + "PAYMENTS_CALCULATE_PAYMENT_FEES_SUCCESS", + "PAYMENTS_CALCULATE_PAYMENT_FEES_FAILURE" +)< + CalculateFeeRequest & { paymentMethodId: string }, + CalculateFeeResponse, + NetworkError +>(); + +export const paymentsCreateTransactionAction = createAsyncAction( + "PAYMENTS_CREATE_TRANSACTION_REQUEST", + "PAYMENTS_CREATE_TRANSACTION_SUCCESS", + "PAYMENTS_CREATE_TRANSACTION_FAILURE" +)< + NewTransactionRequest, + NewTransactionResponse, + NetworkError | WalletPaymentFailure +>(); + +export const paymentsGetPaymentTransactionInfoAction = createAsyncAction( + "PAYMENTS_GET_PAYMENT_TRANSACTION_INFO_REQUEST", + "PAYMENTS_GET_PAYMENT_TRANSACTION_INFO_SUCCESS", + "PAYMENTS_GET_PAYMENT_TRANSACTION_INFO_FAILURE" +)<{ transactionId: string }, TransactionInfo, NetworkError>(); + +export const paymentsDeleteTransactionAction = createAsyncAction( + "PAYMENTS_DELETE_TRANSACTION_REQUEST", + "PAYMENTS_DELETE_TRANSACTION_SUCCESS", + "PAYMENTS_DELETE_TRANSACTION_FAILURE" +)(); + +export type WalletPaymentAuthorizePayload = { + transactionId: string; + walletId: string; + pspId: string; + paymentAmount: AmountEuroCents; + paymentFees: AmountEuroCents; +}; + +export const paymentsStartPaymentAuthorizationAction = createAsyncAction( + "PAYMENTS_START_PAYMENT_AUTH_REQUEST", + "PAYMENTS_START_PAYMENT_AUTH_SUCCESS", + "PAYMENTS_START_PAYMENT_AUTH_FAILURE", + "PAYMENTS_START_PAYMENT_AUTH_CANCEL" +)< + WalletPaymentAuthorizePayload, + RequestAuthorizationResponse, + NetworkError, + undefined +>(); + +export const paymentsResetPaymentPspList = createStandardAction( + "PAYMENTS_RESET_PAYMENT_PSP_LIST" +)(); + +export type PaymentsCheckoutNetworkingActions = + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/payments/checkout/store/actions/orchestration.ts b/ts/features/payments/checkout/store/actions/orchestration.ts new file mode 100644 index 00000000000..339b0d7d890 --- /dev/null +++ b/ts/features/payments/checkout/store/actions/orchestration.ts @@ -0,0 +1,42 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; +import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; +import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo"; +import { PaymentStartOrigin, WalletPaymentStepEnum } from "../../types"; + +export const walletPaymentSetCurrentStep = createStandardAction( + "WALLET_PAYMENT_SET_CURRENT_STEP" +)(); + +export type OnPaymentSuccessAction = "showHome" | "showTransaction"; + +export type PaymentInitStateParams = { + startOrigin?: PaymentStartOrigin; + onSuccess?: OnPaymentSuccessAction; +}; + +/** + * Action to initialize the state of a payment, optionally you can specify the route to go back to + * after the payment is completed or cancelled (default is the popToTop route) + */ +export const initPaymentStateAction = createStandardAction( + "PAYMENTS_INIT_PAYMENT_STATE" +)(); + +export const selectPaymentMethodAction = createStandardAction( + "PAYMENTS_SELECT_PAYMENT_METHOD" +)(); + +export const selectPaymentPspAction = createStandardAction( + "PAYMENTS_SELECT_PAYMENT_PSP" +)(); + +export const resetPaymentPspAction = createStandardAction( + "PAYMENTS_RESET_PAYMENT_PSP" +)(); + +export type PaymentsCheckoutOrchestrationActions = + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/walletV3/payment/store/reducers/index.ts b/ts/features/payments/checkout/store/reducers/index.ts similarity index 57% rename from ts/features/walletV3/payment/store/reducers/index.ts rename to ts/features/payments/checkout/store/reducers/index.ts index 0e9302a0ebd..60e14dcb2ee 100644 --- a/ts/features/walletV3/payment/store/reducers/index.ts +++ b/ts/features/payments/checkout/store/reducers/index.ts @@ -1,40 +1,43 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; -import { NavigatorScreenParams } from "@react-navigation/native"; -import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; +import _ from "lodash"; import { getType } from "typesafe-actions"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; +import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentMethodsResponse"; import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; -import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; -import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; -import NavigationService from "../../../../../navigation/NavigationService"; -import { AppParamsList } from "../../../../../navigation/params/AppParamsList"; +import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo"; +import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets"; import { Action } from "../../../../../store/actions/types"; import { NetworkError } from "../../../../../utils/errors"; -import { WalletPaymentFailure } from "../../types/failure"; +import { WalletPaymentStepEnum } from "../../types"; +import { WalletPaymentFailure } from "../../types/WalletPaymentFailure"; import { - walletPaymentAuthorization, - walletPaymentCalculateFees, - walletPaymentCreateTransaction, - walletPaymentDeleteTransaction, - walletPaymentGetAllMethods, - walletPaymentGetDetails, - walletPaymentGetTransactionInfo, - walletPaymentGetUserWallets, - walletPaymentNewSessionToken + paymentsCalculatePaymentFeesAction, + paymentsCreateTransactionAction, + paymentsDeleteTransactionAction, + paymentsGetNewSessionTokenAction, + paymentsGetPaymentDetailsAction, + paymentsGetPaymentMethodsAction, + paymentsGetPaymentTransactionInfoAction, + paymentsGetPaymentUserMethodsAction, + paymentsResetPaymentPspList, + paymentsStartPaymentAuthorizationAction } from "../actions/networking"; import { - walletPaymentInitState, - walletPaymentPickPaymentMethod, - walletPaymentPickPsp, - walletPaymentResetPickedPsp + OnPaymentSuccessAction, + initPaymentStateAction, + resetPaymentPspAction, + selectPaymentMethodAction, + selectPaymentPspAction, + walletPaymentSetCurrentStep } from "../actions/orchestration"; -export type WalletPaymentState = { +export const WALLET_PAYMENT_STEP_MAX = 4; + +export type PaymentsCheckoutState = { + currentStep: WalletPaymentStepEnum; rptId?: RptId; sessionToken: pot.Pot; paymentDetails: pot.Pot< @@ -48,13 +51,11 @@ export type WalletPaymentState = { chosenPsp: O.Option; transaction: pot.Pot; authorizationUrl: pot.Pot; - startRoute?: { - routeName: keyof AppParamsList; - routeKey: keyof NavigatorScreenParams["screen"]; - }; + onSuccess?: OnPaymentSuccessAction; }; -const INITIAL_STATE: WalletPaymentState = { +const INITIAL_STATE: PaymentsCheckoutState = { + currentStep: WalletPaymentStepEnum.PICK_PAYMENT_METHOD, sessionToken: pot.none, paymentDetails: pot.none, userWallets: pot.none, @@ -68,175 +69,176 @@ const INITIAL_STATE: WalletPaymentState = { // eslint-disable-next-line complexity const reducer = ( - state: WalletPaymentState = INITIAL_STATE, + state: PaymentsCheckoutState = INITIAL_STATE, action: Action -): WalletPaymentState => { +): PaymentsCheckoutState => { switch (action.type) { - case getType(walletPaymentInitState): - const startRoute = pipe( - sequenceS(O.Monad)({ - routeName: O.fromNullable( - NavigationService.getCurrentRouteName() as keyof AppParamsList - ), - routeKey: O.fromNullable( - NavigationService.getCurrentRouteKey() as keyof NavigatorScreenParams["screen"] - ) - }), - O.toUndefined - ); + case getType(initPaymentStateAction): return { ...INITIAL_STATE, - startRoute + onSuccess: action.payload.onSuccess + }; + + case getType(walletPaymentSetCurrentStep): + return { + ...state, + currentStep: _.clamp(action.payload, 1, WALLET_PAYMENT_STEP_MAX) }; // eCommerce Session token - case getType(walletPaymentNewSessionToken.request): + case getType(paymentsGetNewSessionTokenAction.request): return { ...state, sessionToken: pot.toLoading(state.sessionToken) }; - case getType(walletPaymentNewSessionToken.success): + case getType(paymentsGetNewSessionTokenAction.success): return { ...state, sessionToken: pot.some(action.payload.sessionToken) }; - case getType(walletPaymentNewSessionToken.failure): + case getType(paymentsGetNewSessionTokenAction.failure): return { ...state, sessionToken: pot.toError(state.sessionToken, action.payload) }; // Payment verification and details - case getType(walletPaymentGetDetails.request): + case getType(paymentsGetPaymentDetailsAction.request): return { ...state, rptId: action.payload, paymentDetails: pot.toLoading(state.paymentDetails) }; - case getType(walletPaymentGetDetails.success): + case getType(paymentsGetPaymentDetailsAction.success): return { ...state, paymentDetails: pot.some(action.payload) }; - case getType(walletPaymentGetDetails.failure): + case getType(paymentsGetPaymentDetailsAction.failure): return { ...state, paymentDetails: pot.toError(state.paymentDetails, action.payload) }; // User payment methods - case getType(walletPaymentGetUserWallets.request): + case getType(paymentsGetPaymentUserMethodsAction.request): return { ...state, userWallets: pot.toLoading(state.userWallets) }; - case getType(walletPaymentGetUserWallets.success): + case getType(paymentsGetPaymentUserMethodsAction.success): return { ...state, userWallets: pot.some(action.payload) }; - case getType(walletPaymentGetUserWallets.failure): + case getType(paymentsGetPaymentUserMethodsAction.failure): return { ...state, userWallets: pot.toError(state.userWallets, action.payload) }; // Available payment method - case getType(walletPaymentGetAllMethods.request): + case getType(paymentsGetPaymentMethodsAction.request): return { ...state, allPaymentMethods: pot.toLoading(state.allPaymentMethods) }; - case getType(walletPaymentGetAllMethods.success): + case getType(paymentsGetPaymentMethodsAction.success): return { ...state, allPaymentMethods: pot.some(action.payload) }; - case getType(walletPaymentGetAllMethods.failure): + case getType(paymentsGetPaymentMethodsAction.failure): return { ...state, allPaymentMethods: pot.toError(state.allPaymentMethods, action.payload) }; - case getType(walletPaymentPickPaymentMethod): + case getType(selectPaymentMethodAction): return { ...state, chosenPaymentMethod: O.some(action.payload) }; // PSP list - case getType(walletPaymentCalculateFees.request): + case getType(paymentsCalculatePaymentFeesAction.request): return { ...state, pspList: pot.toLoading(state.pspList) }; - case getType(walletPaymentCalculateFees.success): + case getType(paymentsCalculatePaymentFeesAction.success): return { ...state, pspList: pot.some(action.payload.bundles) }; - case getType(walletPaymentCalculateFees.failure): + case getType(paymentsCalculatePaymentFeesAction.failure): return { ...state, pspList: pot.toError(state.pspList, action.payload) }; - case getType(walletPaymentPickPsp): + case getType(selectPaymentPspAction): return { ...state, chosenPsp: O.some(action.payload) }; - case getType(walletPaymentResetPickedPsp): + case getType(resetPaymentPspAction): return { ...state, chosenPsp: O.none }; + case getType(paymentsResetPaymentPspList): + return { + ...state, + pspList: pot.none + }; + // Transaction - case getType(walletPaymentCreateTransaction.request): - case getType(walletPaymentGetTransactionInfo.request): - case getType(walletPaymentDeleteTransaction.request): + case getType(paymentsCreateTransactionAction.request): + case getType(paymentsGetPaymentTransactionInfoAction.request): + case getType(paymentsDeleteTransactionAction.request): return { ...state, transaction: pot.toLoading(state.transaction) }; - case getType(walletPaymentCreateTransaction.success): - case getType(walletPaymentGetTransactionInfo.success): + case getType(paymentsCreateTransactionAction.success): + case getType(paymentsGetPaymentTransactionInfoAction.success): return { ...state, transaction: pot.some(action.payload) }; - case getType(walletPaymentDeleteTransaction.success): + case getType(paymentsDeleteTransactionAction.success): return { ...state, transaction: pot.none }; - case getType(walletPaymentCreateTransaction.failure): - case getType(walletPaymentGetTransactionInfo.failure): - case getType(walletPaymentDeleteTransaction.failure): + case getType(paymentsCreateTransactionAction.failure): + case getType(paymentsGetPaymentTransactionInfoAction.failure): + case getType(paymentsDeleteTransactionAction.failure): return { ...state, transaction: pot.toError(state.transaction, action.payload) }; // Authorization url - case getType(walletPaymentAuthorization.request): + case getType(paymentsStartPaymentAuthorizationAction.request): return { ...state, authorizationUrl: pot.toLoading(state.authorizationUrl) }; - case getType(walletPaymentAuthorization.success): + case getType(paymentsStartPaymentAuthorizationAction.success): return { ...state, authorizationUrl: pot.some(action.payload.authorizationUrl) }; - case getType(walletPaymentAuthorization.failure): + case getType(paymentsStartPaymentAuthorizationAction.failure): return { ...state, authorizationUrl: pot.toError(state.authorizationUrl, action.payload) }; - case getType(walletPaymentAuthorization.cancel): + case getType(paymentsStartPaymentAuthorizationAction.cancel): return { ...state, authorizationUrl: pot.none diff --git a/ts/features/walletV3/payment/store/selectors/index.ts b/ts/features/payments/checkout/store/selectors/index.ts similarity index 51% rename from ts/features/walletV3/payment/store/selectors/index.ts rename to ts/features/payments/checkout/store/selectors/index.ts index bd05aa66f9d..a99e423d871 100644 --- a/ts/features/walletV3/payment/store/selectors/index.ts +++ b/ts/features/payments/checkout/store/selectors/index.ts @@ -4,24 +4,23 @@ import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; import { GlobalState } from "../../../../../store/reducers/types"; -const selectWalletPayment = (state: GlobalState) => - state.features.wallet.payment; +const selectPaymentsCheckoutState = (state: GlobalState) => + state.features.payments.checkout; + +export const selectWalletPaymentCurrentStep = (state: GlobalState) => + selectPaymentsCheckoutState(state).currentStep; export const selectWalletPaymentSessionTokenPot = (state: GlobalState) => - selectWalletPayment(state).sessionToken; + selectPaymentsCheckoutState(state).sessionToken; export const selectWalletPaymentSessionToken = (state: GlobalState) => pot.toUndefined(selectWalletPaymentSessionTokenPot(state)); -export const walletPaymentRptIdSelector = createSelector( - selectWalletPayment, - state => state.rptId -); +export const walletPaymentRptIdSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).rptId; -export const walletPaymentDetailsSelector = createSelector( - selectWalletPayment, - state => state.paymentDetails -); +export const walletPaymentDetailsSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).paymentDetails; export const walletPaymentAmountSelector = createSelector( walletPaymentDetailsSelector, @@ -29,7 +28,7 @@ export const walletPaymentAmountSelector = createSelector( ); export const walletPaymentAllMethodsSelector = createSelector( - selectWalletPayment, + selectPaymentsCheckoutState, state => pot.map(state.allPaymentMethods, _ => _.paymentMethods ?? []) ); @@ -44,7 +43,7 @@ export const walletPaymentGenericMethodByIdSelector = createSelector( ); export const walletPaymentUserWalletsSelector = createSelector( - selectWalletPayment, + selectPaymentsCheckoutState, state => pot.map(state.userWallets, _ => _.wallets ?? []) ); @@ -58,32 +57,20 @@ export const walletPaymentSavedMethodByIdSelector = createSelector( ) ); -export const walletPaymentPickedPaymentMethodSelector = createSelector( - selectWalletPayment, - state => state.chosenPaymentMethod -); +export const walletPaymentPickedPaymentMethodSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).chosenPaymentMethod; -export const walletPaymentPspListSelector = createSelector( - selectWalletPayment, - state => state.pspList -); +export const walletPaymentPspListSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).pspList; -export const walletPaymentPickedPspSelector = createSelector( - selectWalletPayment, - state => state.chosenPsp -); +export const walletPaymentPickedPspSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).chosenPsp; -export const walletPaymentTransactionSelector = createSelector( - selectWalletPayment, - state => state.transaction -); +export const walletPaymentTransactionSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).transaction; -export const walletPaymentAuthorizationUrlSelector = createSelector( - selectWalletPayment, - state => state.authorizationUrl -); +export const walletPaymentAuthorizationUrlSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).authorizationUrl; -export const walletPaymentStartRouteSelector = createSelector( - selectWalletPayment, - state => state.startRoute -); +export const walletPaymentOnSuccessActionSelector = (state: GlobalState) => + selectPaymentsCheckoutState(state).onSuccess; diff --git a/ts/features/payments/checkout/types/PaymentHistory.ts b/ts/features/payments/checkout/types/PaymentHistory.ts new file mode 100644 index 00000000000..e72b6466a35 --- /dev/null +++ b/ts/features/payments/checkout/types/PaymentHistory.ts @@ -0,0 +1,10 @@ +export type PaymentStartOrigin = + | "message" + | "qrcode_scan" + | "poste_datamatrix_scan" + | "manual_insertion" + | "donation"; + +export type PaymentHistory = { + startOrigin?: PaymentStartOrigin; +}; diff --git a/ts/features/walletV3/payment/types/PaymentOutcomeEnum.ts b/ts/features/payments/checkout/types/PaymentOutcomeEnum.ts similarity index 87% rename from ts/features/walletV3/payment/types/PaymentOutcomeEnum.ts rename to ts/features/payments/checkout/types/PaymentOutcomeEnum.ts index dd1ff939ff8..394564ce5f5 100644 --- a/ts/features/walletV3/payment/types/PaymentOutcomeEnum.ts +++ b/ts/features/payments/checkout/types/PaymentOutcomeEnum.ts @@ -16,7 +16,9 @@ export enum WalletPaymentOutcomeEnum { ORDER_NOT_PRESENT = "11", // (should never happen) INVALID_METHOD = "12", // (should never happen) KO_RETRIABLE = "13", // transaction failed - INVALID_SESSION = "14" // transaction failed + INVALID_SESSION = "14", // transaction failed + METHOD_NOT_ENABLED = "15", // payment method not enabled + WAITING_CONFIRMATION_EMAIL = "17" // waiting for confirmation email } export type WalletPaymentOutcome = t.TypeOf; diff --git a/ts/features/walletV3/payment/types/failure.ts b/ts/features/payments/checkout/types/WalletPaymentFailure.ts similarity index 100% rename from ts/features/walletV3/payment/types/failure.ts rename to ts/features/payments/checkout/types/WalletPaymentFailure.ts diff --git a/ts/features/walletV3/payment/types/index.ts b/ts/features/payments/checkout/types/WalletPaymentPspSortType.ts similarity index 100% rename from ts/features/walletV3/payment/types/index.ts rename to ts/features/payments/checkout/types/WalletPaymentPspSortType.ts diff --git a/ts/features/payments/checkout/types/index.ts b/ts/features/payments/checkout/types/index.ts new file mode 100644 index 00000000000..0e9d6fb26fa --- /dev/null +++ b/ts/features/payments/checkout/types/index.ts @@ -0,0 +1,15 @@ +export type WalletPaymentPspSortType = "default" | "name" | "amount"; + +export type PaymentStartOrigin = + | "message" + | "qrcode_scan" + | "poste_datamatrix_scan" + | "manual_insertion" + | "donation"; + +export enum WalletPaymentStepEnum { + NONE = 0, + PICK_PAYMENT_METHOD = 1, + PICK_PSP = 2, + CONFIRM_TRANSACTION = 3 +} diff --git a/ts/features/walletV3/payment/utils/index.ts b/ts/features/payments/checkout/utils/index.ts similarity index 100% rename from ts/features/walletV3/payment/utils/index.ts rename to ts/features/payments/checkout/utils/index.ts diff --git a/ts/features/payments/common/api/client.ts b/ts/features/payments/common/api/client.ts new file mode 100644 index 00000000000..353050b65cc --- /dev/null +++ b/ts/features/payments/common/api/client.ts @@ -0,0 +1,36 @@ +import { createClient } from "../../../../../definitions/pagopa/walletv3/client"; +import { createClient as createECommerceClient } from "../../../../../definitions/pagopa/ecommerce/client"; +import { defaultRetryingFetch } from "../../../../utils/fetch"; + +export const createWalletClient = (baseUrl: string, bearerAuth: string) => + createClient<"bearerAuth">({ + baseUrl, + basePath: "/payment-wallet/v1", + fetchApi: defaultRetryingFetch(), + withDefaults: op => params => { + const paramsWithDefaults = { + ...params, + bearerAuth + } as Parameters[0]; + + return op(paramsWithDefaults); + } + }); + +export const createPaymentClient = (baseUrl: string, token: string) => + createECommerceClient<"walletToken">({ + baseUrl, + basePath: "/ecommerce/io/v1", + fetchApi: defaultRetryingFetch(), + withDefaults: op => params => { + const paramsWithDefaults = { + ...params, + walletToken: token + } as Parameters[0]; + + return op(paramsWithDefaults); + } + }); + +export type PaymentClient = ReturnType; +export type WalletClient = ReturnType; diff --git a/ts/features/payments/common/components/PaymentCard.tsx b/ts/features/payments/common/components/PaymentCard.tsx new file mode 100644 index 00000000000..accb68953f3 --- /dev/null +++ b/ts/features/payments/common/components/PaymentCard.tsx @@ -0,0 +1,221 @@ +import { + H6, + IOColors, + LabelSmallAlt, + VSpacer, + WithTestID +} from "@pagopa/io-app-design-system"; +import { format } from "date-fns"; +import { capitalize } from "lodash"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import Placeholder, { BoxProps } from "rn-placeholder"; +import BPayLogo from "../../../../../img/wallet/payment-methods/bpay_logo_full.svg"; +import PayPalLogo from "../../../../../img/wallet/payment-methods/paypal/paypal_logo_ext.svg"; +import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; +import I18n from "../../../../i18n"; +import { PaymentCardBankLogo } from "./PaymentCardBankLogo"; + +export type PaymentCardProps = { + brand?: string; + abiCode?: string; + hpan?: string; + expireDate?: Date; + holderName?: string; + holderPhone?: string; + holderEmail?: string; +}; + +export type PaymentCardComponentProps = WithTestID< + | ({ + isLoading?: false; + } & PaymentCardProps) + | { + isLoading: true; + } +>; + +const PaymentCard = (props: PaymentCardComponentProps) => { + if (props.isLoading) { + return ; + } + + const cardIcon = props.brand && ( + + ); + + const holderNameText = props.holderName && ( + + {props.holderName} + + ); + + const expireDateText = props.expireDate && ( + + {I18n.t("wallet.creditCard.validUntil", { + expDate: format(props.expireDate, "MM/YY") + })} + + ); + + const maskedEmailText = props.holderEmail && ( + + {props.holderEmail} + + ); + + const maskedPhoneText = props.holderPhone && ( + + {props.holderPhone} + + ); + + const renderBankLogo = () => { + if (props.holderEmail) { + return ( + + + + ); + } + + if (props.holderPhone) { + return ( + + + + ); + } + + if (props.abiCode) { + return ( + + + + ); + } + + if (props.hpan) { + const circuitName = + props.brand || I18n.t("wallet.methodDetails.cardGeneric"); + + return ( +
+ {capitalize(circuitName)} •••• {props.hpan} +
+ ); + } + + return undefined; + }; + + return ( + + + + {renderBankLogo()} + {cardIcon} + + + {holderNameText} + {maskedEmailText} + {maskedPhoneText} + {expireDateText} + + + + ); +}; + +const PaymentCardSkeleton = () => ( + + + + + + + + + + + + + + + + + +); + +const SkeletonPlaceholder = (props: Pick) => ( + +); + +const styleSheet = StyleSheet.create({ + card: { + aspectRatio: 16 / 10, + backgroundColor: IOColors["grey-100"], + borderRadius: 16, + borderWidth: 1, + borderColor: IOColors["grey-200"] + }, + wrapper: { + padding: 16, + paddingTop: 8, + flex: 1, + justifyContent: "space-between" + }, + bankInfo: { + paddingTop: 12 + }, + paymentInfo: { + flexDirection: "row", + justifyContent: "space-between" + }, + additionalInfo: { + justifyContent: "space-between" + } +}); + +export { PaymentCard }; diff --git a/ts/features/payments/common/components/PaymentCardBankLogo.tsx b/ts/features/payments/common/components/PaymentCardBankLogo.tsx new file mode 100644 index 00000000000..17aee8ed5ba --- /dev/null +++ b/ts/features/payments/common/components/PaymentCardBankLogo.tsx @@ -0,0 +1,51 @@ +import { IOColors, WithTestID } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { Image, View } from "react-native"; +import Placeholder from "rn-placeholder"; +import { getBankLogosCdnUri } from "../../../../components/ui/utils/strings"; + +type PaymentCardBankLogoProps = { + abiCode: string; + height: number; + accessibilityLabel?: string; +}; + +export const PaymentCardBankLogo = ({ + abiCode, + height, + accessibilityLabel, + testID +}: WithTestID) => { + const [isLoading, setIsLoading] = React.useState(true); + const [imageWidth, setImageWidth] = React.useState(50); + return ( + + { + const scale = height / nativeEvent.source.height; + setImageWidth(nativeEvent.source.width * scale); + }} + onLoadEnd={() => setIsLoading(false)} + /> + {isLoading && ( + + + + )} + + ); +}; diff --git a/ts/components/ui/cards/payment/PaymentCardBig.tsx b/ts/features/payments/common/components/PaymentCardBig.tsx similarity index 92% rename from ts/components/ui/cards/payment/PaymentCardBig.tsx rename to ts/features/payments/common/components/PaymentCardBig.tsx index ea6d19dc64c..c7eab3d4f63 100644 --- a/ts/components/ui/cards/payment/PaymentCardBig.tsx +++ b/ts/features/payments/common/components/PaymentCardBig.tsx @@ -4,19 +4,19 @@ import { IOColors, IOLogoPaymentExtType, IOStyles, + LabelSmall, LogoPaymentExt, VSpacer } from "@pagopa/io-app-design-system"; import React from "react"; import { StyleSheet, View } from "react-native"; import Placeholder from "rn-placeholder"; +import { LogoPaymentExtended } from "../../../../components/ui/LogoPaymentExtended"; import I18n from "../../../../i18n"; import { WithTestID } from "../../../../types/WithTestID"; import { format } from "../../../../utils/dates"; import { capitalize } from "../../../../utils/strings"; -import { LabelSmall } from "../../../core/typography/LabelSmall"; -import { LogoPaymentExtended } from "../../LogoPaymentExtended"; -import { LogoPaymentWithFallback } from "../../utils/components/LogoPaymentWithFallback"; +import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; export const PaymentCardBig = (props: PaymentCardBigProps) => { if (props.isLoading) { @@ -182,16 +182,22 @@ const BottomSectionText = (props: { string: string; a11yLabel: string }) => ( {props.string} ); -const ExpDateComponent = ({ expDate }: { expDate: Date }) => ( - <> - - - {I18n.t("wallet.creditCard.validUntil", { - expDate: format(expDate, "MM/YY") - })} - - -); +const ExpDateComponent = ({ expDate }: { expDate?: Date }) => { + if (expDate) { + return ( + <> + + + {I18n.t("wallet.creditCard.validUntil", { + expDate: format(expDate, "MM/YY") + })} + + + ); + } + + return null; +}; // ------------- skeleton const CardSkeleton = ({ testID }: { testID?: string }) => ( @@ -259,14 +265,14 @@ type PaymentCardStandardProps = } | { cardType: "PAGOBANCOMAT"; - expirationDate: Date; + expirationDate?: Date; abiCode?: string; holderName: string; bankName?: string; } | { cardType: "COBADGE"; - expirationDate: Date; + expirationDate?: Date; abiCode?: string; bankName?: string; holderName: string; @@ -274,7 +280,7 @@ type PaymentCardStandardProps = } | { cardType: "CREDIT"; - expirationDate: Date; + expirationDate?: Date; holderName: string; hpan: string; cardIcon?: IOLogoPaymentExtType; @@ -292,6 +298,7 @@ const styles = StyleSheet.create({ justifyContent: "space-between", height: 207, borderRadius: 16, + borderCurve: "continuous", backgroundColor: IOColors["grey-100"], padding: 24, width: "100%" // required for consistent skeleton sizing diff --git a/ts/components/ui/cards/payment/PaymentCardSmall.tsx b/ts/features/payments/common/components/PaymentCardSmall.tsx similarity index 89% rename from ts/components/ui/cards/payment/PaymentCardSmall.tsx rename to ts/features/payments/common/components/PaymentCardSmall.tsx index 2ae405f73c9..914e231d689 100644 --- a/ts/components/ui/cards/payment/PaymentCardSmall.tsx +++ b/ts/features/payments/common/components/PaymentCardSmall.tsx @@ -1,20 +1,21 @@ -import * as React from "react"; -import { Pressable, StyleSheet, View } from "react-native"; -import Animated from "react-native-reanimated"; -import Placeholder from "rn-placeholder"; import { + H6, IOColors, - Icon, - VSpacer, IOLogoPaymentType, - H6 + Icon, + VSpacer } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { Pressable, StyleSheet, View } from "react-native"; +import Animated from "react-native-reanimated"; +import Placeholder from "rn-placeholder"; +import { BrandEnum } from "../../../../../definitions/pagopa/ecommerce/WalletInfoDetails"; +import { useSpringPressScaleAnimation } from "../../../../components/ui/utils/hooks/useSpringPressScaleAnimation"; import { WithTestID } from "../../../../types/WithTestID"; -import { LogoPaymentWithFallback } from "../../utils/components/LogoPaymentWithFallback"; -import { useSpringPressScaleAnimation } from "../../utils/hooks/useSpringPressScaleAnimation"; +import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; type RenderData = { - iconName: IOLogoPaymentType | undefined; + iconName: IOLogoPaymentType | BrandEnum | undefined; bottomText: string; }; const getRenderData = (props: CardDataType): RenderData => { @@ -50,7 +51,7 @@ type CardDataType = | { cardType: "CREDIT"; hpan: string; - cardIcon?: IOLogoPaymentType; + cardIcon?: IOLogoPaymentType | BrandEnum; } | { cardType: "PAGOBANCOMAT"; @@ -62,7 +63,7 @@ type CardDataType = | { cardType: "COBADGE"; providerName: string; - cardIcon?: IOLogoPaymentType; + cardIcon?: IOLogoPaymentType | BrandEnum; }; export type PaymentCardSmallProps = WithTestID< @@ -189,6 +190,7 @@ const styles = StyleSheet.create({ flexBasis: PAYMENT_CARD_SMALL_WIDTH, flexGrow: 0, borderRadius: 8, + borderCurve: "continuous", padding: 16 }, logoRow: { diff --git a/ts/components/ui/cards/payment/PaymentCardsCarousel.tsx b/ts/features/payments/common/components/PaymentCardsCarousel.tsx similarity index 92% rename from ts/components/ui/cards/payment/PaymentCardsCarousel.tsx rename to ts/features/payments/common/components/PaymentCardsCarousel.tsx index 7ad7534c66e..c11f47941d7 100644 --- a/ts/components/ui/cards/payment/PaymentCardsCarousel.tsx +++ b/ts/features/payments/common/components/PaymentCardsCarousel.tsx @@ -1,9 +1,9 @@ +import { HSpacer, IOVisualCostants } from "@pagopa/io-app-design-system"; import * as React from "react"; import { StyleSheet } from "react-native"; import { FlatList } from "react-native-gesture-handler"; -import { HSpacer, IOVisualCostants } from "@pagopa/io-app-design-system"; +import { generateFlatListItemLayout } from "../../../../components/ui/utils/generateFlatListItemLayout"; import { WithTestID } from "../../../../types/WithTestID"; -import { generateFlatListItemLayout } from "../../utils/generateFlatListItemLayout"; import { PAYMENT_CARD_SMALL_WIDTH, PaymentCardSmall, @@ -26,6 +26,7 @@ const generateCardsLayout = generateFlatListItemLayout( PAYMENT_CARD_SMALL_WIDTH, DIVIDER_WIDTH ); + export const PaymentCardsCarousel = ({ cards, testID diff --git a/ts/features/payments/common/components/__tests__/PaymentCard.test.tsx b/ts/features/payments/common/components/__tests__/PaymentCard.test.tsx new file mode 100644 index 00000000000..50adb0d3721 --- /dev/null +++ b/ts/features/payments/common/components/__tests__/PaymentCard.test.tsx @@ -0,0 +1,59 @@ +import { render } from "@testing-library/react-native"; +import { format } from "date-fns"; +import { default as React } from "react"; +import I18n from "../../../../../i18n"; +import { PaymentCard } from "../PaymentCard"; + +describe("PaymentCard", () => { + jest.useFakeTimers(); + it(`should match snapshot for loading`, () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + it(`should render credit card data`, () => { + const tDate = new Date(); + const { queryByText } = render( + + ); + + expect(queryByText("Mastercard •••• 1234")).not.toBeNull(); + expect( + queryByText( + I18n.t("wallet.creditCard.validUntil", { + expDate: format(tDate, "MM/YY") + }) + ) + ).not.toBeNull(); + }); + it(`should render bancomat data`, () => { + const tDate = new Date(); + const { queryByTestId, queryByText } = render( + + ); + + expect(queryByTestId("paymentCardBankLogoTestId")).not.toBeNull(); + expect(queryByText("Mastercard •••• 1234")).toBeNull(); + expect(queryByText("Anna Verdi")).not.toBeNull(); + expect( + queryByText( + I18n.t("wallet.creditCard.validUntil", { + expDate: format(tDate, "MM/YY") + }) + ) + ).not.toBeNull(); + }); + it(`should render BPay data`, () => { + const { queryByTestId, queryByText } = render( + + ); + expect(queryByTestId("paymentCardBPayLogoTestId")).not.toBeNull(); + expect(queryByText("123456789")).not.toBeNull(); + }); + it(`should render PayPal data`, () => { + const { queryByTestId, queryByText } = render( + + ); + expect(queryByTestId("paymentCardPayPalLogoTestId")).not.toBeNull(); + expect(queryByText("abc@abc.it")).not.toBeNull(); + }); +}); diff --git a/ts/features/payments/common/components/__tests__/PaymentCardBig.test.tsx b/ts/features/payments/common/components/__tests__/PaymentCardBig.test.tsx new file mode 100644 index 00000000000..6ab8e29da3c --- /dev/null +++ b/ts/features/payments/common/components/__tests__/PaymentCardBig.test.tsx @@ -0,0 +1,53 @@ +import { default as React } from "react"; +import { createStore } from "redux"; +import ROUTES from "../../../../../navigation/routes"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { PaymentCardBig, PaymentCardBigProps } from "../PaymentCardBig"; + +describe("PaymentCardBig component", () => { + const testID = "PaymentCardBigTestID"; + jest.useFakeTimers(); + it(`matches snapshot for loading`, () => { + const component = renderCardBig({ isLoading: true }); + expect(component).toMatchSnapshot(); + }); + it(`matches snapshot for paypal`, () => { + const component = renderCardBig({ + cardType: "PAYPAL", + holderEmail: "someEmail@test.com" + }); + expect(component).toMatchSnapshot(); + }); + + it(`should render a phone number in case of BancomatPay`, () => { + const component = renderCardBig({ + cardType: "BANCOMATPAY", + phoneNumber: "1234567890", + holderName: "holderName", + testID + }); + expect(component).not.toBeNull(); + expect(component.queryByText("1234567890")).not.toBeNull(); + }); + it(`should render a skeleton when loading`, () => { + const component = renderCardBig({ + isLoading: true, + testID + }); + expect(component).not.toBeNull(); + expect(component.queryByTestId(`${testID}-skeleton`)).not.toBeNull(); + }); +}); + +function renderCardBig(props: PaymentCardBigProps) { + const globalState = appReducer(undefined, applicationChangeState("active")); + return renderScreenWithNavigationStoreContext( + () => , + ROUTES.WALLET_HOME, + {}, + createStore(appReducer, globalState as any) + ); +} diff --git a/ts/components/ui/cards/payment/__test__/PaymentCards.test.tsx b/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx similarity index 61% rename from ts/components/ui/cards/payment/__test__/PaymentCards.test.tsx rename to ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx index 2d711bcf39f..fcf5118c7da 100644 --- a/ts/components/ui/cards/payment/__test__/PaymentCards.test.tsx +++ b/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import { default as React } from "react"; import { createStore } from "redux"; import ROUTES from "../../../../../navigation/routes"; import { applicationChangeState } from "../../../../../store/actions/application"; @@ -6,7 +6,6 @@ import { appReducer } from "../../../../../store/reducers"; import { GlobalState } from "../../../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; import { PaymentCardSmall, PaymentCardSmallProps } from "../PaymentCardSmall"; -import { PaymentCardBig, PaymentCardBigProps } from "../PaymentCardBig"; describe("PaymentCardSmall component", () => { const testID = "PaymentCardSmallTestID"; @@ -57,51 +56,6 @@ describe("PaymentCardSmall component", () => { }); }); -describe("PaymentCardBig component", () => { - const testID = "PaymentCardBigTestID"; - jest.useFakeTimers(); - it(`matches snapshot for loading`, () => { - const component = renderCardBig({ isLoading: true }); - expect(component).toMatchSnapshot(); - }); - it(`matches snapshot for paypal`, () => { - const component = renderCardBig({ - cardType: "PAYPAL", - holderEmail: "someEmail@test.com" - }); - expect(component).toMatchSnapshot(); - }); - - it(`should render a phone number in case of BancomatPay`, () => { - const component = renderCardBig({ - cardType: "BANCOMATPAY", - phoneNumber: "1234567890", - holderName: "holderName", - testID - }); - expect(component).not.toBeNull(); - expect(component.queryByText("1234567890")).not.toBeNull(); - }); - it(`should render a skeleton when loading`, () => { - const component = renderCardBig({ - isLoading: true, - testID - }); - expect(component).not.toBeNull(); - expect(component.queryByTestId(`${testID}-skeleton`)).not.toBeNull(); - }); -}); - -function renderCardBig(props: PaymentCardBigProps) { - const globalState = appReducer(undefined, applicationChangeState("active")); - return renderScreenWithNavigationStoreContext( - () => , - ROUTES.WALLET_HOME, - {}, - createStore(appReducer, globalState as any) - ); -} - function renderCardSmall(props: PaymentCardSmallProps) { const globalState = appReducer(undefined, applicationChangeState("active")); return renderScreenWithNavigationStoreContext( diff --git a/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCard.test.tsx.snap b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCard.test.tsx.snap new file mode 100644 index 00000000000..8a74210a395 --- /dev/null +++ b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCard.test.tsx.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentCard should match snapshot for loading 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardBig.test.tsx.snap b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardBig.test.tsx.snap new file mode 100644 index 00000000000..b28621aed7b --- /dev/null +++ b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardBig.test.tsx.snap @@ -0,0 +1,898 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentCardBig component matches snapshot for loading 1`] = ` + + + + + + + + + + + + + + + WALLET_HOME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`PaymentCardBig component matches snapshot for paypal 1`] = ` + + + + + + + + + + + + + + + WALLET_HOME + + + + + + + + + + + + + + + + + + + + + someEmail@test.com + + + + + + + + + + + + + +`; diff --git a/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap new file mode 100644 index 00000000000..817dc8922fc --- /dev/null +++ b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap @@ -0,0 +1,916 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentCardSmall component matches snapshot for loading 1`] = ` + + + + + + + + + + + + + + + WALLET_HOME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`PaymentCardSmall component matches snapshot for paypal 1`] = ` + + + + + + + + + + + + + + + WALLET_HOME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PayPal + + + + + + + + + + + + + +`; diff --git a/ts/components/ui/utils/components/BankLogoOrLoadingSkeleton.tsx b/ts/features/payments/common/components/utils/BankLogoOrLoadingSkeleton.tsx similarity index 96% rename from ts/components/ui/utils/components/BankLogoOrLoadingSkeleton.tsx rename to ts/features/payments/common/components/utils/BankLogoOrLoadingSkeleton.tsx index 1eef3efc585..06826e87fd0 100644 --- a/ts/components/ui/utils/components/BankLogoOrLoadingSkeleton.tsx +++ b/ts/features/payments/common/components/utils/BankLogoOrLoadingSkeleton.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { Image } from "react-native"; import Placeholder from "rn-placeholder"; import { IOColors } from "@pagopa/io-app-design-system"; -import { getBankLogosCdnUri } from "../strings"; +import { getBankLogosCdnUri } from "../../../../../components/ui/utils/strings"; type BankLogoOrSkeletonProps = { abiCode?: string; diff --git a/ts/features/payments/common/saga/index.ts b/ts/features/payments/common/saga/index.ts new file mode 100644 index 00000000000..c68b76f3a6a --- /dev/null +++ b/ts/features/payments/common/saga/index.ts @@ -0,0 +1,27 @@ +import { SagaIterator } from "redux-saga"; +import { fork, select } from "typed-redux-saga/macro"; +import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; +import { watchPaymentsOnboardingSaga } from "../../onboarding/saga"; +import { watchPaymentsCheckoutSaga } from "../../checkout/saga"; +import { createPaymentClient, createWalletClient } from "../api/client"; +import { walletApiBaseUrl, walletApiUatBaseUrl } from "../../../../config"; +import { watchPaymentsMethodDetailsSaga } from "../../details/saga"; +import { watchPaymentsTransactionSaga } from "../../transaction/saga"; +import { watchPaymentsWalletSaga } from "../../wallet/saga"; + +export function* watchPaymentsSaga(walletToken: string): SagaIterator { + const isPagoPATestEnabled = yield* select(isPagoPATestEnabledSelector); + + const walletBaseUrl = isPagoPATestEnabled + ? walletApiUatBaseUrl + : walletApiBaseUrl; + + const walletClient = createWalletClient(walletBaseUrl, walletToken); + const paymentClient = createPaymentClient(walletBaseUrl, walletToken); + + yield* fork(watchPaymentsWalletSaga, walletClient); + yield* fork(watchPaymentsOnboardingSaga, walletClient); + yield* fork(watchPaymentsMethodDetailsSaga, walletClient); + yield* fork(watchPaymentsTransactionSaga, walletClient); + yield* fork(watchPaymentsCheckoutSaga, paymentClient); +} diff --git a/ts/features/payments/common/store/actions/index.ts b/ts/features/payments/common/store/actions/index.ts new file mode 100644 index 00000000000..5a30d06b2b2 --- /dev/null +++ b/ts/features/payments/common/store/actions/index.ts @@ -0,0 +1,14 @@ +import { PaymentsMethodDetailsActions } from "../../../details/store/actions"; +import { PaymentsHistoryActions } from "../../../history/store/actions"; +import { PaymentsOnboardingActions } from "../../../onboarding/store/actions"; +import { PaymentsCheckoutActions } from "../../../checkout/store/actions"; +import { PaymentsTransactionActions } from "../../../transaction/store/actions"; +import { PaymentsWalletActions } from "../../../wallet/store/actions"; + +export type PaymentsActions = + | PaymentsOnboardingActions + | PaymentsMethodDetailsActions + | PaymentsCheckoutActions + | PaymentsTransactionActions + | PaymentsHistoryActions + | PaymentsWalletActions; diff --git a/ts/features/payments/common/store/reducers/index.ts b/ts/features/payments/common/store/reducers/index.ts new file mode 100644 index 00000000000..44845a5e129 --- /dev/null +++ b/ts/features/payments/common/store/reducers/index.ts @@ -0,0 +1,40 @@ +import { combineReducers } from "redux"; +import { PersistPartial } from "redux-persist"; +import paymentReducer, { + PaymentsCheckoutState +} from "../../../checkout/store/reducers"; +import detailsReducer, { + PaymentsMethodDetailsState +} from "../../../details/store/reducers"; +import historyReducer, { + PaymentsHistoryState +} from "../../../history/store/reducers"; +import onboardingReducer, { + PaymentsOnboardingState +} from "../../../onboarding/store/reducers"; +import transactionReducer, { + PaymentsTransactionState +} from "../../../transaction/store/reducers"; +import paymentsWalletReducer, { + PaymentsWalletState +} from "../../../wallet/store/reducers"; + +export type PaymentsState = { + onboarding: PaymentsOnboardingState; + details: PaymentsMethodDetailsState; + checkout: PaymentsCheckoutState; + transaction: PaymentsTransactionState; + history: PaymentsHistoryState & PersistPartial; + wallet: PaymentsWalletState; +}; + +const paymentsReducer = combineReducers({ + onboarding: onboardingReducer, + details: detailsReducer, + checkout: paymentReducer, + transaction: transactionReducer, + history: historyReducer, + wallet: paymentsWalletReducer +}); + +export default paymentsReducer; diff --git a/ts/features/walletV3/details/types/UIWalletInfoDetails.ts b/ts/features/payments/common/types/UIWalletInfoDetails.ts similarity index 93% rename from ts/features/walletV3/details/types/UIWalletInfoDetails.ts rename to ts/features/payments/common/types/UIWalletInfoDetails.ts index 56f40ccbd47..5ba809c575e 100644 --- a/ts/features/walletV3/details/types/UIWalletInfoDetails.ts +++ b/ts/features/payments/common/types/UIWalletInfoDetails.ts @@ -3,7 +3,7 @@ import { WalletInfoDetails1, WalletInfoDetails2, WalletInfoDetails3 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; +} from "../../../../../definitions/pagopa/ecommerce/WalletInfoDetails"; /** * Transforms all required props from WalletInfoDetails1 to partial diff --git a/ts/features/walletV3/common/utils/const.ts b/ts/features/payments/common/utils/const.ts similarity index 100% rename from ts/features/walletV3/common/utils/const.ts rename to ts/features/payments/common/utils/const.ts diff --git a/ts/features/walletV3/common/utils/index.ts b/ts/features/payments/common/utils/index.ts similarity index 85% rename from ts/features/walletV3/common/utils/index.ts rename to ts/features/payments/common/utils/index.ts index 812c3a3b7b6..61d88efd94e 100644 --- a/ts/features/walletV3/common/utils/index.ts +++ b/ts/features/payments/common/utils/index.ts @@ -8,14 +8,13 @@ import { pipe } from "fp-ts/lib/function"; import I18n from "i18n-js"; import _ from "lodash"; import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; -import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; -import { ServiceStatusEnum } from "../../../../../definitions/pagopa/walletv3/ServiceStatus"; +import { WalletApplicationStatusEnum } from "../../../../../definitions/pagopa/walletv3/WalletApplicationStatus"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; import { isExpiredDate } from "../../../../utils/dates"; import { findFirstCaseInsensitive } from "../../../../utils/object"; -import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; -import { WalletPaymentPspSortType } from "../../payment/types"; +import { WalletPaymentPspSortType } from "../../checkout/types"; +import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; /** * A simple function to get the corresponding translated badge text, @@ -60,21 +59,21 @@ export const isPaymentMethodExpired = ( * @param paymentMethod * @param walletFunction */ -export const hasServiceEnabled = ( +export const hasApplicationEnabled = ( paymentMethod: WalletInfo | undefined, - walletService: ServiceNameEnum + walletApplication: string ): boolean => paymentMethod !== undefined && - paymentMethod.services.some( - service => - service.name === walletService && - service.status === ServiceStatusEnum.ENABLED + paymentMethod.applications.some( + application => + application.name === walletApplication && + application.status === WalletApplicationStatusEnum.ENABLED ); /** * return true if the payment method has the payment feature */ export const hasPaymentFeature = (paymentMethod: WalletInfo): boolean => - hasServiceEnabled(paymentMethod, ServiceNameEnum.PAGOPA); + hasApplicationEnabled(paymentMethod, "PAGOPA"); /** * Check if a payment method is supported or not @@ -111,7 +110,7 @@ export const getPaymentLogo = ( return "payPal"; } else if (details.maskedNumber !== undefined) { return "bancomatPay"; - } else if (details.maskedPan !== undefined) { + } else if (details.lastFourDigits !== undefined) { return pipe( details.brand, O.fromNullable, diff --git a/ts/features/walletV3/common/utils/validation.ts b/ts/features/payments/common/utils/validation.ts similarity index 100% rename from ts/features/walletV3/common/utils/validation.ts rename to ts/features/payments/common/utils/validation.ts diff --git a/ts/features/payments/common/utils/wallet.ts b/ts/features/payments/common/utils/wallet.ts new file mode 100644 index 00000000000..33e4a1dea21 --- /dev/null +++ b/ts/features/payments/common/utils/wallet.ts @@ -0,0 +1,26 @@ +import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; +import { getDateFromExpiryDate } from "../../../../utils/dates"; +import { WalletCard } from "../../../newWallet/types"; +import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; + +export const mapWalletIdToCardKey = (walletId: string) => `method_${walletId}`; + +export const mapWalletsToCards = ( + wallets: ReadonlyArray +): ReadonlyArray => + wallets.map(wallet => { + const details = wallet.details as UIWalletInfoDetails; + + return { + key: mapWalletIdToCardKey(wallet.walletId), + type: "payment", + category: "payment", + walletId: wallet.walletId, + hpan: details.lastFourDigits, + abiCode: details.abi, + brand: details.brand, + expireDate: getDateFromExpiryDate(details.expiryDate), + holderEmail: details.maskedEmail, + holderPhone: details.maskedNumber + }; + }); diff --git a/ts/features/payments/details/components/PaymentsMethodDetailsBaseScreenComponent.tsx b/ts/features/payments/details/components/PaymentsMethodDetailsBaseScreenComponent.tsx new file mode 100644 index 00000000000..4eb96c4628d --- /dev/null +++ b/ts/features/payments/details/components/PaymentsMethodDetailsBaseScreenComponent.tsx @@ -0,0 +1,81 @@ +import { + ContentWrapper, + IOColors, + IOSpacingScale, + VSpacer +} from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { StyleSheet, View } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; +import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; +import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; +import { useIOSelector } from "../../../../store/hooks"; +import { isDesignSystemEnabledSelector } from "../../../../store/reducers/persistedPreferences"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { + PaymentCard, + PaymentCardComponentProps +} from "../../common/components/PaymentCard"; + +type Props = { + card: PaymentCardComponentProps; + headerTitle?: string; +}; + +/** + * Base layout for payment methods screen & legacy delete handling + */ +const PaymentsMethodDetailsBaseScreenComponent = ({ + card, + headerTitle = "", + children +}: React.PropsWithChildren) => { + const isDSenabled = useIOSelector(isDesignSystemEnabledSelector); + const blueHeaderColor = isDSenabled ? IOColors["blueIO-600"] : IOColors.blue; + + return ( + + + + + + + + + + {children} + + + + ); +}; + +const cardContainerHorizontalSpacing: IOSpacingScale = 16; + +const styles = StyleSheet.create({ + cardContainer: { + paddingHorizontal: cardContainerHorizontalSpacing, + alignSelf: "center", + marginBottom: "-15%", + aspectRatio: 1.7, + width: "100%" + }, + blueHeader: { + paddingTop: "75%", + marginTop: "-75%", + marginBottom: "15%" + } +}); + +export { PaymentsMethodDetailsBaseScreenComponent }; diff --git a/ts/features/payments/details/components/PaymentsMethodDetailsDeleteButton.tsx b/ts/features/payments/details/components/PaymentsMethodDetailsDeleteButton.tsx new file mode 100644 index 00000000000..d92d4dcf950 --- /dev/null +++ b/ts/features/payments/details/components/PaymentsMethodDetailsDeleteButton.tsx @@ -0,0 +1,77 @@ +import { IOToast, ListItemAction } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { Alert, Platform } from "react-native"; +import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch } from "../../../../store/hooks"; +import { paymentsDeleteMethodAction } from "../store/actions"; + +type PaymentsDetailsDeleteMethodButtonProps = { + paymentMethod?: WalletInfo; +}; + +const PaymentsMethodDetailsDeleteButton = ({ + paymentMethod +}: PaymentsDetailsDeleteMethodButtonProps) => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + + const deleteWallet = (walletId: string) => { + dispatch( + paymentsDeleteMethodAction.request({ + walletId, + onSuccess: () => { + IOToast.success(I18n.t("wallet.delete.successful")); + }, + onFailure: () => { + IOToast.error(I18n.t("wallet.delete.failed")); + } + }) + ); + navigation.goBack(); + }; + + if (paymentMethod === undefined) { + return null; + } + + const onDeleteMethod = () => { + // Create a native Alert to confirm or cancel the delete action + Alert.alert( + I18n.t("wallet.newRemove.title"), + I18n.t("wallet.newRemove.body"), + [ + { + text: + Platform.OS === "ios" + ? I18n.t(`wallet.delete.ios.confirm`) + : I18n.t(`wallet.delete.android.confirm`), + style: "destructive", + onPress: () => { + if (paymentMethod) { + deleteWallet(paymentMethod.walletId); + } + } + }, + { + text: I18n.t("global.buttons.cancel"), + style: "default" + } + ], + { cancelable: false } + ); + }; + + return ( + + ); +}; + +export { PaymentsMethodDetailsDeleteButton }; diff --git a/ts/features/payments/details/components/PaymentsMethodDetailsErrorContent.tsx b/ts/features/payments/details/components/PaymentsMethodDetailsErrorContent.tsx new file mode 100644 index 00000000000..61cc1f7d76e --- /dev/null +++ b/ts/features/payments/details/components/PaymentsMethodDetailsErrorContent.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch } from "../../../../store/hooks"; +import { paymentsGetMethodDetailsAction } from "../store/actions"; + +type Props = { + walletId: string; +}; + +const PaymentsMethodDetailsErrorContent = ({ walletId }: Props) => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + + React.useLayoutEffect(() => { + navigation.setOptions({ + header: undefined + }); + }, [navigation]); + + const handleOnRetry = () => { + dispatch(paymentsGetMethodDetailsAction.request({ walletId })); + }; + + return ( + navigation.pop() + }} + secondaryAction={{ + label: I18n.t("wallet.methodDetails.error.secondaryButton"), + accessibilityLabel: I18n.t( + "wallet.methodDetails.error.secondaryButton" + ), + onPress: handleOnRetry + }} + /> + ); +}; + +export { PaymentsMethodDetailsErrorContent }; diff --git a/ts/features/walletV3/common/components/WalletDetailsPagoPaPaymentCapability.tsx b/ts/features/payments/details/components/WalletDetailsPagoPaPaymentCapability.tsx similarity index 82% rename from ts/features/walletV3/common/components/WalletDetailsPagoPaPaymentCapability.tsx rename to ts/features/payments/details/components/WalletDetailsPagoPaPaymentCapability.tsx index 7150259d5ed..d404890d0b7 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPagoPaPaymentCapability.tsx +++ b/ts/features/payments/details/components/WalletDetailsPagoPaPaymentCapability.tsx @@ -1,12 +1,10 @@ +import { IOToast, ListItemSwitch } from "@pagopa/io-app-design-system"; import * as React from "react"; -import { ListItemSwitch } from "@pagopa/io-app-design-system"; - -import { hasPaymentFeature } from "../utils"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import I18n from "../../../../i18n"; import { useIODispatch } from "../../../../store/hooks"; -import { walletDetailsPagoPaCapabilityToggle } from "../../details/store/actions"; -import { IOToast } from "../../../../components/Toast"; +import { hasPaymentFeature } from "../../common/utils"; +import { paymentsTogglePagoPaCapabilityAction } from "../../details/store/actions"; type Props = { paymentMethod: WalletInfo }; @@ -35,7 +33,7 @@ const WalletDetailsPagoPaPaymentCapability: React.FC = props => { const handleSwitchPagoPaCapability = () => { setLoading(true); dispatch( - walletDetailsPagoPaCapabilityToggle.request({ + paymentsTogglePagoPaCapabilityAction.request({ walletId: props.paymentMethod.walletId, onSuccess: handleSwitchSuccess, onFailure: handleSwitchError diff --git a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx b/ts/features/payments/details/components/WalletDetailsPaymentMethodFeatures.tsx similarity index 95% rename from ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx rename to ts/features/payments/details/components/WalletDetailsPaymentMethodFeatures.tsx index b3799b6f200..5a7f3e3b7c7 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx +++ b/ts/features/payments/details/components/WalletDetailsPaymentMethodFeatures.tsx @@ -1,13 +1,11 @@ import { Alert } from "@pagopa/io-app-design-system"; import * as React from "react"; import { View } from "react-native"; - -import I18n from "../../../../i18n"; -import { isIdPayEnabledSelector } from "../../../../store/reducers/backendStatus"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; - -import { isPaymentMethodExpired } from "../utils"; +import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; +import { isIdPayEnabledSelector } from "../../../../store/reducers/backendStatus"; +import { isPaymentMethodExpired } from "../../common/utils"; import PaymentMethodInitiatives from "./WalletDetailsPaymentMethodInitiatives"; import PaymentMethodSettings from "./WalletDetailsPaymentMethodSettings"; diff --git a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodInitiatives.tsx b/ts/features/payments/details/components/WalletDetailsPaymentMethodInitiatives.tsx similarity index 94% rename from ts/features/walletV3/common/components/WalletDetailsPaymentMethodInitiatives.tsx rename to ts/features/payments/details/components/WalletDetailsPaymentMethodInitiatives.tsx index 652a98bebfe..1fef37d47b2 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodInitiatives.tsx +++ b/ts/features/payments/details/components/WalletDetailsPaymentMethodInitiatives.tsx @@ -2,6 +2,7 @@ import { Body, H6, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; import { useFocusEffect, useNavigation } from "@react-navigation/native"; import * as React from "react"; import { View } from "react-native"; +import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import I18n from "../../../../i18n"; import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../../../navigation/params/WalletParamsList"; @@ -9,12 +10,10 @@ import ROUTES from "../../../../navigation/routes"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { IdPayInstrumentInitiativesList } from "../../../idpay/wallet/components/IdPayInstrumentInitiativesList"; import { - idPayInitiativesFromInstrumentGet, idPayInitiativesFromInstrumentRefreshStart, idPayInitiativesFromInstrumentRefreshStop } from "../../../idpay/wallet/store/actions"; import { idPayEnabledInitiativesFromInstrumentSelector } from "../../../idpay/wallet/store/reducers"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; type Props = { paymentMethod: WalletInfo; @@ -34,11 +33,6 @@ const WalletDetailsPaymentMethodInitiatives = ( const dispatch = useIODispatch(); const startInitiativeRefreshPolling = React.useCallback(() => { - dispatch( - idPayInitiativesFromInstrumentGet.request({ - idWallet: idWalletString - }) - ); dispatch( idPayInitiativesFromInstrumentRefreshStart({ idWallet: idWalletString diff --git a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodSettings.tsx b/ts/features/payments/details/components/WalletDetailsPaymentMethodSettings.tsx similarity index 99% rename from ts/features/walletV3/common/components/WalletDetailsPaymentMethodSettings.tsx rename to ts/features/payments/details/components/WalletDetailsPaymentMethodSettings.tsx index 6916a71958d..587e335baee 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodSettings.tsx +++ b/ts/features/payments/details/components/WalletDetailsPaymentMethodSettings.tsx @@ -1,10 +1,9 @@ -import * as React from "react"; import { H6, IOSpacingScale } from "@pagopa/io-app-design-system"; - +import * as React from "react"; +import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import FavoritePaymentMethodSwitch from "../../../../components/wallet/FavoriteMethodSwitch"; import I18n from "../../../../i18n"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import WalletDetailsPagoPaPaymentCapability from "./WalletDetailsPagoPaPaymentCapability"; type Props = { paymentMethod: WalletInfo }; diff --git a/ts/features/payments/details/navigation/navigator.tsx b/ts/features/payments/details/navigation/navigator.tsx new file mode 100644 index 00000000000..95660ec0116 --- /dev/null +++ b/ts/features/payments/details/navigation/navigator.tsx @@ -0,0 +1,36 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import PaymentsMethodDetailsScreen from "../screens/PaymentsMethodDetailsScreen"; +import { PaymentsMethodDetailsParamsList } from "./params"; +import { PaymentsMethodDetailsRoutes } from "./routes"; + +const Stack = createStackNavigator(); + +export const PaymentsMethodDetailsNavigator = () => ( + + + +); + +export type PaymentsMethodDetailsStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type PaymentsMethodDetailsStackNavigation = + PaymentsMethodDetailsStackNavigationProp< + PaymentsMethodDetailsParamsList, + keyof PaymentsMethodDetailsParamsList + >; diff --git a/ts/features/payments/details/navigation/params.ts b/ts/features/payments/details/navigation/params.ts new file mode 100644 index 00000000000..329415e93aa --- /dev/null +++ b/ts/features/payments/details/navigation/params.ts @@ -0,0 +1,7 @@ +import { PaymentsMethodDetailsScreenNavigationParams } from "../screens/PaymentsMethodDetailsScreen"; +import { PaymentsMethodDetailsRoutes } from "./routes"; + +export type PaymentsMethodDetailsParamsList = { + [PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_NAVIGATOR]: undefined; + [PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_SCREEN]: PaymentsMethodDetailsScreenNavigationParams; +}; diff --git a/ts/features/payments/details/navigation/routes.ts b/ts/features/payments/details/navigation/routes.ts new file mode 100644 index 00000000000..9553f81c833 --- /dev/null +++ b/ts/features/payments/details/navigation/routes.ts @@ -0,0 +1,4 @@ +export const PaymentsMethodDetailsRoutes = { + PAYMENT_METHOD_DETAILS_NAVIGATOR: "PAYMENT_METHOD_DETAILS_NAVIGATOR", + PAYMENT_METHOD_DETAILS_SCREEN: "PAYMENT_METHOD_DETAILS_SCREEN" +} as const; diff --git a/ts/features/walletV3/details/saga/handleDeleteWalletDetails.ts b/ts/features/payments/details/saga/handleDeleteWalletDetails.ts similarity index 69% rename from ts/features/walletV3/details/saga/handleDeleteWalletDetails.ts rename to ts/features/payments/details/saga/handleDeleteWalletDetails.ts index 3cc41ff8576..bf68cf9bdae 100644 --- a/ts/features/walletV3/details/saga/handleDeleteWalletDetails.ts +++ b/ts/features/payments/details/saga/handleDeleteWalletDetails.ts @@ -3,13 +3,15 @@ import * as E from "fp-ts/lib/Either"; import { ActionType } from "typesafe-actions"; import { SagaCallReturnType } from "../../../../types/utils"; import { - walletDetailsDeleteInstrument, - walletDetailsGetInstrument + paymentsDeleteMethodAction, + paymentsGetMethodDetailsAction } from "../store/actions"; import { readablePrivacyReport } from "../../../../utils/reporters"; import { getGenericError, getNetworkError } from "../../../../utils/errors"; import { WalletClient } from "../../common/api/client"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { walletRemoveCards } from "../../../newWallet/store/actions/cards"; +import { mapWalletIdToCardKey } from "../../common/utils/wallet"; /** * Handle the remote call to start Wallet onboarding payment methods list @@ -18,7 +20,7 @@ import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; */ export function* handleDeleteWalletDetails( deleteWalletById: WalletClient["deleteWalletById"], - action: ActionType<(typeof walletDetailsDeleteInstrument)["request"]> + action: ActionType<(typeof paymentsDeleteMethodAction)["request"]> ) { try { const deleteWalletRequest = deleteWalletById({ @@ -31,37 +33,43 @@ export function* handleDeleteWalletDetails( )) as unknown as SagaCallReturnType; if (E.isRight(deleteWalletResult)) { if (deleteWalletResult.right.status === 204) { + yield* put( + walletRemoveCards([mapWalletIdToCardKey(action.payload.walletId)]) + ); + // handled success - const successAction = walletDetailsDeleteInstrument.success(); + const successAction = paymentsDeleteMethodAction.success(); yield* put(successAction); if (action.payload.onSuccess) { - action.payload.onSuccess(successAction); + action.payload.onSuccess(); } return; } // not handled error codes - const failureAction = walletDetailsDeleteInstrument.failure({ + const failureAction = paymentsDeleteMethodAction.failure({ ...getGenericError( new Error(`response status code ${deleteWalletResult.right.status}`) ) }); yield* put(failureAction); if (action.payload.onFailure) { - action.payload.onFailure(failureAction); + action.payload.onFailure(); } } else { // cannot decode response - const failureAction = walletDetailsDeleteInstrument.failure({ + const failureAction = paymentsDeleteMethodAction.failure({ ...getGenericError( new Error(readablePrivacyReport(deleteWalletResult.left)) ) }); yield* put(failureAction); if (action.payload.onFailure) { - action.payload.onFailure(failureAction); + action.payload.onFailure(); } } } catch (e) { - yield* put(walletDetailsGetInstrument.failure({ ...getNetworkError(e) })); + yield* put( + paymentsGetMethodDetailsAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/details/saga/handleGetWalletDetails.ts b/ts/features/payments/details/saga/handleGetWalletDetails.ts similarity index 68% rename from ts/features/walletV3/details/saga/handleGetWalletDetails.ts rename to ts/features/payments/details/saga/handleGetWalletDetails.ts index d6780933f2d..4020dd2abac 100644 --- a/ts/features/walletV3/details/saga/handleGetWalletDetails.ts +++ b/ts/features/payments/details/saga/handleGetWalletDetails.ts @@ -2,11 +2,13 @@ import { call, put } from "typed-redux-saga/macro"; import * as E from "fp-ts/lib/Either"; import { ActionType } from "typesafe-actions"; import { SagaCallReturnType } from "../../../../types/utils"; -import { walletDetailsGetInstrument } from "../store/actions"; +import { paymentsGetMethodDetailsAction } from "../store/actions"; import { readablePrivacyReport } from "../../../../utils/reporters"; import { getGenericError, getNetworkError } from "../../../../utils/errors"; import { WalletClient } from "../../common/api/client"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { walletAddCards } from "../../../newWallet/store/actions/cards"; +import { mapWalletsToCards } from "../../common/utils/wallet"; /** * Handle the remote call to start Wallet onboarding payment methods list @@ -15,7 +17,7 @@ import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; */ export function* handleGetWalletDetails( getWalletById: WalletClient["getWalletById"], - action: ActionType<(typeof walletDetailsGetInstrument)["request"]> + action: ActionType<(typeof paymentsGetMethodDetailsAction)["request"]> ) { try { const getwalletDetailsRequest = getWalletById({ @@ -28,15 +30,24 @@ export function* handleGetWalletDetails( )) as unknown as SagaCallReturnType; if (E.isRight(getWalletDetailsResult)) { if (getWalletDetailsResult.right.status === 200) { + // Upsert the card in the wallet + yield* put( + walletAddCards( + mapWalletsToCards([getWalletDetailsResult.right.value]) + ) + ); + // handled success yield* put( - walletDetailsGetInstrument.success(getWalletDetailsResult.right.value) + paymentsGetMethodDetailsAction.success( + getWalletDetailsResult.right.value + ) ); return; } // not handled error codes yield* put( - walletDetailsGetInstrument.failure({ + paymentsGetMethodDetailsAction.failure({ ...getGenericError( new Error( `response status code ${getWalletDetailsResult.right.status}` @@ -47,7 +58,7 @@ export function* handleGetWalletDetails( } else { // cannot decode response yield* put( - walletDetailsGetInstrument.failure({ + paymentsGetMethodDetailsAction.failure({ ...getGenericError( new Error(readablePrivacyReport(getWalletDetailsResult.left)) ) @@ -55,6 +66,8 @@ export function* handleGetWalletDetails( ); } } catch (e) { - yield* put(walletDetailsGetInstrument.failure({ ...getNetworkError(e) })); + yield* put( + paymentsGetMethodDetailsAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/payments/details/saga/handleTogglePagoPaCapability.ts b/ts/features/payments/details/saga/handleTogglePagoPaCapability.ts new file mode 100644 index 00000000000..5255ff7c1fe --- /dev/null +++ b/ts/features/payments/details/saga/handleTogglePagoPaCapability.ts @@ -0,0 +1,96 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as E from "fp-ts/lib/Either"; +import { call, put, select } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { WalletApplication } from "../../../../../definitions/pagopa/walletv3/WalletApplication"; +import { WalletApplicationStatusEnum } from "../../../../../definitions/pagopa/walletv3/WalletApplicationStatus"; +import { SagaCallReturnType } from "../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../utils/reporters"; +import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { WalletClient } from "../../common/api/client"; +import { + paymentsGetMethodDetailsAction, + paymentsTogglePagoPaCapabilityAction +} from "../store/actions"; +import { selectPaymentMethodDetails } from "../store/selectors"; + +/** + * Handle the remote call to toggle the Wallet pagopa capability + */ +export function* handleTogglePagoPaCapability( + updateWalletApplicationsById: WalletClient["updateWalletApplicationsById"], + action: ActionType<(typeof paymentsTogglePagoPaCapabilityAction)["request"]> +) { + try { + const methodDetailsPot = yield* select(selectPaymentMethodDetails); + const methodDetails = pot.toUndefined(methodDetailsPot); + + if (!methodDetails) { + throw new Error("walletDetails is undefined"); + } + const updatedApplications = methodDetails.applications.map(application => ({ + ...application, + status: updatePagoPaApplicationStatus(application) + })); + + const updateWalletPagoPaApplicationRequest = updateWalletApplicationsById({ + walletId: action.payload.walletId, + body: { + applications: updatedApplications as Array + } + }); + const updateWalletResult = (yield* call( + withRefreshApiCall, + updateWalletPagoPaApplicationRequest, + action + )) as unknown as SagaCallReturnType; + if (E.isRight(updateWalletResult)) { + if (updateWalletResult.right.status === 204) { + // handled success + const successAction = paymentsTogglePagoPaCapabilityAction.success(); + yield* put(successAction); + if (action.payload.onSuccess) { + action.payload.onSuccess(); + } + return; + } + // not handled error codes + const failureAction = paymentsTogglePagoPaCapabilityAction.failure({ + ...getGenericError( + new Error(`response status code ${updateWalletResult.right.status}`) + ) + }); + yield* put(failureAction); + if (action.payload.onFailure) { + action.payload.onFailure(); + } + } else { + // cannot decode response + const failureAction = paymentsTogglePagoPaCapabilityAction.failure({ + ...getGenericError( + new Error(readablePrivacyReport(updateWalletResult.left)) + ) + }); + yield* put(failureAction); + if (action.payload.onFailure) { + action.payload.onFailure(); + } + } + } catch (e) { + yield* put( + paymentsGetMethodDetailsAction.failure({ ...getNetworkError(e) }) + ); + } +} + +const updatePagoPaApplicationStatus = ( + application: WalletApplication +): WalletApplicationStatusEnum | undefined => { + if (application.name === "PAGOPA") { + return application.status === WalletApplicationStatusEnum.DISABLED + ? WalletApplicationStatusEnum.ENABLED + : WalletApplicationStatusEnum.DISABLED; + } + return application.status; +}; diff --git a/ts/features/walletV3/details/saga/index.ts b/ts/features/payments/details/saga/index.ts similarity index 66% rename from ts/features/walletV3/details/saga/index.ts rename to ts/features/payments/details/saga/index.ts index 13562e3dab9..b26ffdfc353 100644 --- a/ts/features/walletV3/details/saga/index.ts +++ b/ts/features/payments/details/saga/index.ts @@ -1,41 +1,40 @@ import { SagaIterator } from "redux-saga"; import { takeLatest } from "typed-redux-saga/macro"; - import { WalletClient } from "../../common/api/client"; import { - walletDetailsDeleteInstrument, - walletDetailsGetInstrument, - walletDetailsPagoPaCapabilityToggle + paymentsDeleteMethodAction, + paymentsGetMethodDetailsAction, + paymentsTogglePagoPaCapabilityAction } from "../store/actions"; -import { handleGetWalletDetails } from "./handleGetWalletDetails"; import { handleDeleteWalletDetails } from "./handleDeleteWalletDetails"; +import { handleGetWalletDetails } from "./handleGetWalletDetails"; import { handleTogglePagoPaCapability } from "./handleTogglePagoPaCapability"; /** - * Handle Wallet onboarding requests - * @param bearerToken + * Handle payment method onboarding requests + * @param walletClient wallet client */ -export function* watchWalletDetailsSaga( +export function* watchPaymentsMethodDetailsSaga( walletClient: WalletClient ): SagaIterator { // handle the request of get wallet details yield* takeLatest( - walletDetailsGetInstrument.request, + paymentsGetMethodDetailsAction.request, handleGetWalletDetails, walletClient.getWalletById ); // handle the request of delete a wallet yield* takeLatest( - walletDetailsDeleteInstrument.request, + paymentsDeleteMethodAction.request, handleDeleteWalletDetails, walletClient.deleteWalletById ); // handle request to a wallet yield* takeLatest( - walletDetailsPagoPaCapabilityToggle.request, + paymentsTogglePagoPaCapabilityAction.request, handleTogglePagoPaCapability, - walletClient.updateWalletServicesById + walletClient.updateWalletApplicationsById ); } diff --git a/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx b/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx new file mode 100644 index 00000000000..ef2b65f0327 --- /dev/null +++ b/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx @@ -0,0 +1,113 @@ +import { VSpacer } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { RouteProp, useRoute } from "@react-navigation/native"; +import * as React from "react"; +import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { isIdPayEnabledSelector } from "../../../../store/reducers/backendStatus"; +import { getDateFromExpiryDate } from "../../../../utils/dates"; +import { capitalize } from "../../../../utils/strings"; +import { idPayInitiativesFromInstrumentGet } from "../../../idpay/wallet/store/actions"; +import { idPayAreInitiativesFromInstrumentLoadingSelector } from "../../../idpay/wallet/store/reducers"; +import { PaymentCardProps } from "../../common/components/PaymentCard"; +import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { PaymentsMethodDetailsBaseScreenComponent } from "../components/PaymentsMethodDetailsBaseScreenComponent"; +import { PaymentsMethodDetailsDeleteButton } from "../components/PaymentsMethodDetailsDeleteButton"; +import { PaymentsMethodDetailsErrorContent } from "../components/PaymentsMethodDetailsErrorContent"; +import WalletDetailsPaymentMethodFeatures from "../components/WalletDetailsPaymentMethodFeatures"; +import { PaymentsMethodDetailsParamsList } from "../navigation/params"; +import { paymentsGetMethodDetailsAction } from "../store/actions"; +import { selectPaymentMethodDetails } from "../store/selectors"; + +export type PaymentsMethodDetailsScreenNavigationParams = Readonly<{ + walletId: string; +}>; + +export type PaymentsMethodDetailsScreenRouteProps = RouteProp< + PaymentsMethodDetailsParamsList, + "PAYMENT_METHOD_DETAILS_SCREEN" +>; + +const PaymentsMethodDetailsScreen = () => { + const route = useRoute(); + const dispatch = useIODispatch(); + + const { walletId } = route.params; + + const isIdpayEnabled = useIOSelector(isIdPayEnabledSelector); + const walletDetailsPot = useIOSelector(selectPaymentMethodDetails); + const areIdpayInitiativesLoading = useIOSelector( + idPayAreInitiativesFromInstrumentLoadingSelector + ); + + const isLoading = + pot.isLoading(walletDetailsPot) || + pot.isUpdating(walletDetailsPot) || + areIdpayInitiativesLoading; + + React.useEffect(() => { + dispatch(paymentsGetMethodDetailsAction.request({ walletId })); + if (isIdpayEnabled) { + dispatch( + idPayInitiativesFromInstrumentGet.request({ + idWallet: walletId + }) + ); + } + }, [walletId, dispatch, isIdpayEnabled]); + + if (isLoading) { + return ( + + ); + } + + if (pot.isSome(walletDetailsPot) && !isLoading) { + const paymentMethod = walletDetailsPot.value; + const cardProps = getPaymentCardPropsFromWallet(paymentMethod); + const headerTitle = getCardHeaderTitle(paymentMethod.details); + + return ( + + + + + + ); + } + + return ; +}; + +const getCardHeaderTitle = (details?: UIWalletInfoDetails) => { + if (details?.lastFourDigits !== undefined) { + const capitalizedCardCircuit = capitalize( + details.brand?.toLowerCase() ?? "" + ); + return `${capitalizedCardCircuit} ••${details.lastFourDigits}`; + } + + return ""; +}; + +const getPaymentCardPropsFromWallet = ( + wallet: WalletInfo +): PaymentCardProps => { + const details = wallet.details as UIWalletInfoDetails; + + return { + hpan: details.lastFourDigits, + abiCode: details.abi, + brand: details.brand, + expireDate: getDateFromExpiryDate(details.expiryDate), + holderEmail: details.maskedEmail, + holderPhone: details.maskedNumber + }; +}; + +export default PaymentsMethodDetailsScreen; diff --git a/ts/features/payments/details/store/actions/index.ts b/ts/features/payments/details/store/actions/index.ts new file mode 100644 index 00000000000..334cca5658a --- /dev/null +++ b/ts/features/payments/details/store/actions/index.ts @@ -0,0 +1,41 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../../utils/errors"; +import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; + +export const paymentsGetMethodDetailsAction = createAsyncAction( + "PAYMENTS_GET_METHOD_DETAILS_REQUEST", + "PAYMENTS_GET_METHOD_DETAILS_SUCCESS", + "PAYMENTS_GET_METHOD_DETAILS_FAILURE", + "PAYMENTS_GET_METHOD_DETAILS_CANCEL" +)<{ walletId: string }, WalletInfo, NetworkError, void>(); + +type DeleteMethodPayload = { + walletId: string; + onSuccess?: () => void; + onFailure?: () => void; +}; + +export const paymentsDeleteMethodAction = createAsyncAction( + "PAYMENTS_DELETE_METHOD_REQUEST", + "PAYMENTS_DELETE_METHOD_SUCCESS", + "PAYMENTS_DELETE_METHOD_FAILURE", + "PAYMENTS_DELETE_METHOD_CANCEL" +)(); + +type TogglePagoPaCapabilityPayload = { + walletId: string; + onSuccess?: () => void; + onFailure?: () => void; +}; + +export const paymentsTogglePagoPaCapabilityAction = createAsyncAction( + "PAYMENTS_TOGGLE_PAGOPA_CAPABILITY_REQUEST", + "PAYMENTS_TOGGLE_PAGOPA_CAPABILITY_SUCCESS", + "PAYMENTS_TOGGLE_PAGOPA_CAPABILITY_FAILURE", + "PAYMENTS_TOGGLE_PAGOPA_CAPABILITY_CANCEL" +)(); + +export type PaymentsMethodDetailsActions = + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/payments/details/store/reducers/index.ts b/ts/features/payments/details/store/reducers/index.ts new file mode 100644 index 00000000000..3b51c3dbd72 --- /dev/null +++ b/ts/features/payments/details/store/reducers/index.ts @@ -0,0 +1,79 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../../store/actions/types"; +import { NetworkError } from "../../../../../utils/errors"; + +import { WalletApplicationStatusEnum } from "../../../../../../definitions/pagopa/walletv3/WalletApplicationStatus"; +import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; +import { + paymentsGetMethodDetailsAction, + paymentsTogglePagoPaCapabilityAction +} from "../actions"; + +export type PaymentsMethodDetailsState = { + walletDetails: pot.Pot; +}; + +const INITIAL_STATE: PaymentsMethodDetailsState = { + walletDetails: pot.noneLoading +}; + +const reducer = ( + state: PaymentsMethodDetailsState = INITIAL_STATE, + action: Action +): PaymentsMethodDetailsState => { + switch (action.type) { + // GET METHOD DETAILS + case getType(paymentsGetMethodDetailsAction.request): + return { + ...state, + walletDetails: pot.toLoading(pot.none) + }; + case getType(paymentsGetMethodDetailsAction.success): + return { + ...state, + walletDetails: pot.some(action.payload) + }; + case getType(paymentsGetMethodDetailsAction.failure): + return { + ...state, + walletDetails: pot.toError(state.walletDetails, action.payload) + }; + case getType(paymentsGetMethodDetailsAction.cancel): + return { + ...state, + walletDetails: pot.none + }; + + // TOGGLE PAGOPA CAPABILITY + case getType(paymentsTogglePagoPaCapabilityAction.success): + const walletDetails = pot.getOrElse( + state.walletDetails, + {} as WalletInfo + ); + const updatedApplications = walletDetails.applications.map( + application => { + if (application.name === "PAGOPA") { + return { + ...application, + status: + application.status === WalletApplicationStatusEnum.ENABLED + ? WalletApplicationStatusEnum.DISABLED + : WalletApplicationStatusEnum.ENABLED + }; + } + return application; + } + ); + return { + ...state, + walletDetails: pot.some({ + ...walletDetails, + applications: updatedApplications + }) + }; + } + return state; +}; + +export default reducer; diff --git a/ts/features/payments/details/store/selectors/index.ts b/ts/features/payments/details/store/selectors/index.ts new file mode 100644 index 00000000000..e846309394c --- /dev/null +++ b/ts/features/payments/details/store/selectors/index.ts @@ -0,0 +1,4 @@ +import { GlobalState } from "../../../../../store/reducers/types"; + +export const selectPaymentMethodDetails = (state: GlobalState) => + state.features.payments.details.walletDetails; diff --git a/ts/features/payments/history/store/__tests__/store.test.ts b/ts/features/payments/history/store/__tests__/store.test.ts new file mode 100644 index 00000000000..b63a97fa5da --- /dev/null +++ b/ts/features/payments/history/store/__tests__/store.test.ts @@ -0,0 +1,86 @@ +import MockDate from "mockdate"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { initPaymentStateAction } from "../../../checkout/store/actions/orchestration"; +import { PaymentStartOrigin } from "../../../checkout/types"; +import { + selectOngoingPaymentHistory, + selectPaymentsHistoryArchive +} from "../selectors"; +import { storePaymentOutcomeToHistory } from "../actions"; +import { WalletPaymentOutcomeEnum } from "../../../checkout/types/PaymentOutcomeEnum"; + +const MOCKED_LOOKUP_ID = "123456"; +const MOCKED_DATE = new Date(); + +jest.mock("../../../../../utils/pmLookUpId", () => ({ + getLookUpId: () => MOCKED_LOOKUP_ID +})); + +describe("Test Wallet payment history reducers and selectors", () => { + beforeAll(() => { + jest.useFakeTimers(); + MockDate.set(MOCKED_DATE); + }); + + it("should have INITIAL_STATE before any dispatched action", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + expect(globalState.features.payments.history).toStrictEqual({ + archive: [] + }); + expect(selectPaymentsHistoryArchive(globalState)).toStrictEqual([]); + expect(selectOngoingPaymentHistory(globalState)).toBeUndefined(); + }); + + it("should correctly update ongoing payment history", () => { + const T_START_ORIGIN: PaymentStartOrigin = "manual_insertion"; + + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, globalState as any); + store.dispatch( + initPaymentStateAction({ + startOrigin: T_START_ORIGIN + }) + ); + + expect( + store.getState().features.payments.history.ongoingPayment + ).toStrictEqual({ + startOrigin: T_START_ORIGIN, + lookupId: MOCKED_LOOKUP_ID, + startedAt: MOCKED_DATE + }); + expect(store.getState().features.payments.history.archive).toStrictEqual( + [] + ); + }); + + it("should correctly update payment outcome on SUCCESS", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, globalState as any); + store.dispatch( + storePaymentOutcomeToHistory(WalletPaymentOutcomeEnum.SUCCESS) + ); + expect( + store.getState().features.payments.history.ongoingPayment?.outcome + ).toBe(WalletPaymentOutcomeEnum.SUCCESS); + expect( + store.getState().features.payments.history.ongoingPayment?.success + ).toBe(true); + }); + + it("should correctly update payment outcome on failure", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, globalState as any); + store.dispatch( + storePaymentOutcomeToHistory(WalletPaymentOutcomeEnum.CANCELED_BY_USER) + ); + expect( + store.getState().features.payments.history.ongoingPayment?.outcome + ).toBe(WalletPaymentOutcomeEnum.CANCELED_BY_USER); + expect( + store.getState().features.payments.history.ongoingPayment?.success + ).toBeUndefined(); + }); +}); diff --git a/ts/features/payments/history/store/actions/index.ts b/ts/features/payments/history/store/actions/index.ts new file mode 100644 index 00000000000..149f95151db --- /dev/null +++ b/ts/features/payments/history/store/actions/index.ts @@ -0,0 +1,15 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; +import { WalletPaymentOutcome } from "../../../checkout/types/PaymentOutcomeEnum"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; + +export const storeNewPaymentAttemptAction = createStandardAction( + "PAYMENTS_STORE_NEW_PAYMENT_ATTEMPT" +)(); + +export const storePaymentOutcomeToHistory = createStandardAction( + "PAYMENTS_STORE_OUTCMOE_TO_HISTORY" +)(); + +export type PaymentsHistoryActions = + | ActionType + | ActionType; diff --git a/ts/features/payments/history/store/reducers/index.ts b/ts/features/payments/history/store/reducers/index.ts new file mode 100644 index 00000000000..b1fb7aa47cf --- /dev/null +++ b/ts/features/payments/history/store/reducers/index.ts @@ -0,0 +1,158 @@ +import * as O from "fp-ts/lib/Option"; +import * as A from "fp-ts/lib/Array"; +import { pipe } from "fp-ts/lib/function"; +import _ from "lodash"; +import { AsyncStorage } from "react-native"; +import { PersistConfig, persistReducer } from "redux-persist"; +import { getType } from "typesafe-actions"; +import { differentProfileLoggedIn } from "../../../../../store/actions/crossSessions"; +import { clearCache } from "../../../../../store/actions/profile"; +import { Action } from "../../../../../store/actions/types"; +import { getLookUpId } from "../../../../../utils/pmLookUpId"; +import { + paymentsCreateTransactionAction, + paymentsGetPaymentDetailsAction, + paymentsGetPaymentTransactionInfoAction +} from "../../../checkout/store/actions/networking"; +import { initPaymentStateAction } from "../../../checkout/store/actions/orchestration"; +import { WalletPaymentFailure } from "../../../checkout/types/WalletPaymentFailure"; +import { PaymentHistory } from "../../types"; +import { + storeNewPaymentAttemptAction, + storePaymentOutcomeToHistory +} from "../actions"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; + +export type PaymentsHistoryState = { + ongoingPayment?: PaymentHistory; + archive: ReadonlyArray; +}; + +const INITIAL_STATE: PaymentsHistoryState = { + archive: [] +}; + +export const ARCHIVE_SIZE = 15; + +const reducer = ( + state: PaymentsHistoryState = INITIAL_STATE, + action: Action +): PaymentsHistoryState => { + switch (action.type) { + case getType(initPaymentStateAction): + return { + ...state, + ongoingPayment: { + startOrigin: action.payload.startOrigin, + startedAt: new Date(), + lookupId: getLookUpId() + } + }; + case getType(paymentsGetPaymentDetailsAction.request): + return { + ...state, + ongoingPayment: { + ...state.ongoingPayment, + rptId: action.payload, + attempt: getPaymentAttemptByRptId(state, action.payload) + } + }; + case getType(paymentsGetPaymentDetailsAction.success): + return { + ...state, + ongoingPayment: { + ...state.ongoingPayment, + verifiedData: action.payload + } + }; + case getType(storeNewPaymentAttemptAction): + return updatePaymentHistory(state, {}, true); + case getType(paymentsCreateTransactionAction.success): + case getType(paymentsGetPaymentTransactionInfoAction.success): + return updatePaymentHistory(state, { + transaction: action.payload + }); + case getType(storePaymentOutcomeToHistory): + return updatePaymentHistory(state, { + outcome: action.payload, + ...(action.payload === "0" ? { success: true } : {}) + }); + case getType(paymentsGetPaymentDetailsAction.failure): + case getType(paymentsCreateTransactionAction.failure): + case getType(paymentsGetPaymentTransactionInfoAction.failure): + return updatePaymentHistory(state, { + failure: pipe( + WalletPaymentFailure.decode(action.payload), + O.fromEither, + O.toUndefined + ) + }); + case getType(differentProfileLoggedIn): + case getType(clearCache): + return INITIAL_STATE; + } + return state; +}; + +const getPaymentAttemptByRptId = (state: PaymentsHistoryState, rptId: RptId) => + pipe( + state.archive as Array, + A.findFirst(h => h.rptId === rptId), + O.chainNullableK(h => h.attempt), + O.getOrElse(() => 0) + ); + +const appendItemToArchive = ( + archive: ReadonlyArray, + item: PaymentHistory +): ReadonlyArray => + pipe( + archive, + // Remove previous entry if already exists + a => a.filter(({ rptId }) => !_.isEqual(rptId, item.rptId)), + // Keep only the latest ARCHIVE_SIZE - 1 entries + a => a.slice(-ARCHIVE_SIZE + 1), + // Add the new entry to the archive + a => [...a, item] + ); + +const updatePaymentHistory = ( + state: PaymentsHistoryState, + data: PaymentHistory, + newAttempt: boolean = false +): PaymentsHistoryState => { + const currentAttempt = state.ongoingPayment?.attempt || 0; + const updatedOngoingPaymentHistory: PaymentHistory = { + ...state.ongoingPayment, + ...data, + attempt: newAttempt ? currentAttempt + 1 : currentAttempt + }; + + if (newAttempt) { + return { + ongoingPayment: updatedOngoingPaymentHistory, + archive: appendItemToArchive(state.archive, updatedOngoingPaymentHistory) + }; + } + + return { + ongoingPayment: updatedOngoingPaymentHistory, + archive: [..._.dropRight(state.archive), updatedOngoingPaymentHistory] + }; +}; + +const CURRENT_REDUX_PAYMENT_HISTORY_STORE_VERSION = -1; + +const persistConfig: PersistConfig = { + key: "paymentHistory", + storage: AsyncStorage, + version: CURRENT_REDUX_PAYMENT_HISTORY_STORE_VERSION, + whitelist: ["archive"] +}; + +export const walletPaymentHistoryPersistor = persistReducer< + PaymentsHistoryState, + Action +>(persistConfig, reducer); + +export default walletPaymentHistoryPersistor; diff --git a/ts/features/payments/history/store/selectors/index.ts b/ts/features/payments/history/store/selectors/index.ts new file mode 100644 index 00000000000..5e2198a64b0 --- /dev/null +++ b/ts/features/payments/history/store/selectors/index.ts @@ -0,0 +1,30 @@ +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { createSelector } from "reselect"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import { GlobalState } from "../../../../../store/reducers/types"; + +export const selectPaymentsHistoryArchive = (state: GlobalState) => + state.features.payments.history.archive; + +export const selectOngoingPaymentHistory = (state: GlobalState) => + state.features.payments.history.ongoingPayment; + +export const selectPymentAttemptByRptId = (rptId: RptId) => + createSelector(selectPaymentsHistoryArchive, archive => + pipe( + O.fromNullable(archive.find(h => h.rptId === rptId)), + O.chainNullableK(h => h.attempt), + O.getOrElse(() => 0) + ) + ); + +export const selectOngoingPaymentAttempt = createSelector( + selectOngoingPaymentHistory, + paymentHistory => + pipe( + O.fromNullable(paymentHistory), + O.chainNullableK(h => h.attempt), + O.getOrElse(() => 0) + ) +); diff --git a/ts/features/payments/history/types/index.ts b/ts/features/payments/history/types/index.ts new file mode 100644 index 00000000000..6be5689d01e --- /dev/null +++ b/ts/features/payments/history/types/index.ts @@ -0,0 +1,19 @@ +import { NewTransactionResponse } from "../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; +import { RptId } from "../../../../../definitions/pagopa/ecommerce/RptId"; +import { PaymentStartOrigin } from "../../checkout/types"; +import { WalletPaymentOutcomeEnum } from "../../checkout/types/PaymentOutcomeEnum"; +import { WalletPaymentFailure } from "../../checkout/types/WalletPaymentFailure"; + +export type PaymentHistory = { + startOrigin?: PaymentStartOrigin; + rptId?: RptId; + startedAt?: Date; + lookupId?: string; + verifiedData?: PaymentRequestsGetResponse; + transaction?: NewTransactionResponse; + outcome?: WalletPaymentOutcomeEnum; + failure?: WalletPaymentFailure; + success?: true; + attempt?: number; +}; diff --git a/ts/features/payments/home/components/PaymentsHomeListItemTransaction.tsx b/ts/features/payments/home/components/PaymentsHomeListItemTransaction.tsx new file mode 100644 index 00000000000..53cd770c78a --- /dev/null +++ b/ts/features/payments/home/components/PaymentsHomeListItemTransaction.tsx @@ -0,0 +1,44 @@ +import { ListItemTransaction } from "@pagopa/io-app-design-system"; +import React from "react"; +import I18n from "../../../../i18n"; +import { Transaction, isSuccessTransaction } from "../../../../types/pagopa"; +import { getAccessibleAmountText } from "../../../../utils/accessibility"; +import { format } from "../../../../utils/dates"; +import { formatNumberCurrencyCents } from "../../../idpay/common/utils/strings"; + +type Props = { + transaction: Transaction; +}; + +const PaymentsHomeListItemTransaction = ({ transaction }: Props) => { + const recipient = transaction.merchant; + + const amountText = formatNumberCurrencyCents(transaction.amount.amount); + const datetime: string = format(transaction.created, "DD MMM YYYY, HH:mm"); + + const accessibleDatetime: string = format( + transaction.created, + "DD MMMM YYYY, HH:mm" + ); + const accessibleAmountText = getAccessibleAmountText(amountText); + const accessibilityLabel = `${recipient}; ${accessibleDatetime}; ${accessibleAmountText}`; + + const transactionStatus = + isSuccessTransaction(transaction) === true ? "success" : "failure"; + + return ( + + ); +}; + +export { PaymentsHomeListItemTransaction }; diff --git a/ts/features/payments/home/components/PaymentsHomeTransactionList.tsx b/ts/features/payments/home/components/PaymentsHomeTransactionList.tsx new file mode 100644 index 00000000000..666ff2d9f2a --- /dev/null +++ b/ts/features/payments/home/components/PaymentsHomeTransactionList.tsx @@ -0,0 +1,79 @@ +import { + ListItemHeader, + ListItemTransaction +} from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { useFocusEffect } from "@react-navigation/native"; +import * as _ from "lodash"; +import * as React from "react"; +import { default as I18n } from "../../../../i18n"; +import { fetchTransactionsRequestWithExpBackoff } from "../../../../store/actions/wallet/transactions"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { GlobalState } from "../../../../store/reducers/types"; +import { Transaction } from "../../../../types/pagopa"; +import { PaymentsHomeListItemTransaction } from "./PaymentsHomeListItemTransaction"; + +const PaymentsHomeTransactionList = () => { + const dispatch = useIODispatch(); + + const transactionsPot = useIOSelector( + (state: GlobalState) => state.wallet.transactions.transactions + ); + + const isLoading = pot.isLoading(transactionsPot); + + useFocusEffect( + React.useCallback(() => { + dispatch(fetchTransactionsRequestWithExpBackoff({ start: 0 })); + }, [dispatch]) + ); + + const renderItems = () => { + if (!isLoading && pot.isSome(transactionsPot)) { + const toArray = _.values(transactionsPot.value); + const sortedByCreationDate = _.orderBy(toArray, item => item?.created, [ + "desc" + ]); + + return sortedByCreationDate + .filter((item): item is Transaction => item !== undefined) + .map(transaction => ( + + )); + } + + return Array.from({ length: 5 }).map((_, index) => ( + + )); + }; + + return ( + // full pages history loading will be handled by history details page + <> + null + } + }} + /> + {renderItems()} + + ); +}; + +export { PaymentsHomeTransactionList }; diff --git a/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx b/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx new file mode 100644 index 00000000000..bd421c1c657 --- /dev/null +++ b/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx @@ -0,0 +1,105 @@ +import { ListItemHeader } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { useFocusEffect } from "@react-navigation/native"; +import * as React from "react"; +import { StyleSheet, View } from "react-native"; +import { WalletInfo } from "../../../../../definitions/pagopa/ecommerce/WalletInfo"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { paymentsGetPaymentUserMethodsAction } from "../../checkout/store/actions/networking"; +import { PaymentCardSmallProps } from "../../common/components/PaymentCardSmall"; +import { PaymentCardsCarousel } from "../../common/components/PaymentCardsCarousel"; +import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { PaymentsOnboardingRoutes } from "../../onboarding/navigation/routes"; +import { walletPaymentUserWalletsSelector } from "../../checkout/store/selectors"; +const loadingCards: Array = Array.from({ + length: 3 +}).map(() => ({ + isLoading: true +})); + +type PaymentMethodsSectionProps = { + isLoading?: boolean; +}; + +const PaymentsHomeUserMethodsList = ({ + isLoading +}: PaymentMethodsSectionProps) => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + const paymentMethodsPot = useIOSelector(walletPaymentUserWalletsSelector); + const isLoadingSection = isLoading || pot.isLoading(paymentMethodsPot); + const methods = pot.getOrElse(paymentMethodsPot, []); + + useFocusEffect( + React.useCallback(() => { + dispatch(paymentsGetPaymentUserMethodsAction.request()); + }, [dispatch]) + ); + + const mapMethods = ( + // this function is here to allow future navigation usage + method: NonNullable + ): PaymentCardSmallProps | undefined => { + const details = method.details as UIWalletInfoDetails; + + if (details.lastFourDigits !== undefined) { + return { + cardType: "CREDIT", + hpan: details.lastFourDigits, + cardIcon: details.brand, + isLoading: false + }; + } + if (details.maskedEmail !== undefined) { + return { + cardType: "PAYPAL" + }; + } + if (details.maskedNumber !== undefined) { + return { + cardType: "BANCOMATPAY" + }; + } + return undefined; + }; + + const renderMethods = isLoadingSection + ? loadingCards + : methods + .map(mapMethods) + .filter((item): item is PaymentCardSmallProps => item !== undefined) ?? + loadingCards; + + const handleOnAddMethodPress = () => { + navigation.navigate(PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR, { + screen: PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD + }); + }; + + return ( + <> + + + + + + ); +}; + +const styles = StyleSheet.create({ + fixedCardsHeight: { height: 96 } +}); + +export { PaymentsHomeUserMethodsList }; diff --git a/ts/features/payments/home/screens/PaymentsHomeScreen.tsx b/ts/features/payments/home/screens/PaymentsHomeScreen.tsx new file mode 100644 index 00000000000..738994d3324 --- /dev/null +++ b/ts/features/payments/home/screens/PaymentsHomeScreen.tsx @@ -0,0 +1,34 @@ +import { GradientScrollView, VSpacer } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { PaymentsBarcodeRoutes } from "../../barcode/navigation/routes"; +import { PaymentsHomeTransactionList } from "../components/PaymentsHomeTransactionList"; +import { PaymentsHomeUserMethodsList } from "../components/PaymentsHomeUserMethodsList"; + +export const PaymentsHomeScreen = () => { + const navigation = useIONavigation(); + + const handleOnPayNoticedPress = () => { + navigation.navigate(PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR, { + screen: PaymentsBarcodeRoutes.PAYMENT_BARCODE_SCAN + }); + }; + + return ( + + + + + + ); +}; diff --git a/ts/features/walletV3/onboarding/components/WalletOnboardingPaymentMethodsList.tsx b/ts/features/payments/onboarding/components/WalletOnboardingPaymentMethodsList.tsx similarity index 59% rename from ts/features/walletV3/onboarding/components/WalletOnboardingPaymentMethodsList.tsx rename to ts/features/payments/onboarding/components/WalletOnboardingPaymentMethodsList.tsx index c6596e727a3..d79c4df7091 100644 --- a/ts/features/walletV3/onboarding/components/WalletOnboardingPaymentMethodsList.tsx +++ b/ts/features/payments/onboarding/components/WalletOnboardingPaymentMethodsList.tsx @@ -2,56 +2,53 @@ * This component will display the payment methods that can be registered * on the app */ -import * as React from "react"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import { Divider, - IOLogoPaymentType, - IOPaymentLogos, IOStyles, ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import * as React from "react"; import { FlatList } from "react-native"; -import { WalletPaymentMethodItemSkeleton } from "../../common/components/WalletPaymentMethodItemSkeleton"; import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; -import { findFirstCaseInsensitive } from "../../../../utils/object"; +import { useIOSelector } from "../../../../store/hooks"; +import { selectPaymentOnboardingSelectedMethod } from "../store/selectors"; +import { WalletPaymentMethodItemSkeleton } from "./WalletPaymentMethodItemSkeleton"; type OwnProps = Readonly<{ paymentMethods: ReadonlyArray; onSelectPaymentMethod: (paymentMethod: PaymentMethodResponse) => void; - isLoading?: boolean; - header?: React.ReactElement; + isLoadingMethods?: boolean; + isLoadingWebView?: boolean; }>; type PaymentMethodItemProps = { paymentMethod: PaymentMethodResponse; + isLoading?: boolean; onPress: () => void; }; const PaymentMethodItem = ({ paymentMethod, + isLoading, onPress }: PaymentMethodItemProps) => { const listItemNavCommonProps: ListItemNav = { accessibilityLabel: paymentMethod.description, onPress, + loading: isLoading, value: paymentMethod.description }; return pipe( paymentMethod.asset, O.fromNullable, - O.chain(findFirstCaseInsensitive(IOPaymentLogos)), - O.map(([brand]) => brand), O.fold( () => , brand => ( - + ) ) ); @@ -63,25 +60,34 @@ const PaymentMethodItem = ({ const WalletOnboardingPaymentMethodsList = ({ paymentMethods, onSelectPaymentMethod, - isLoading, - header -}: OwnProps) => ( - item.name} - ListHeaderComponent={header} - ListFooterComponent={renderListFooter(isLoading)} - ItemSeparatorComponent={() => } - renderItem={({ item }) => ( - onSelectPaymentMethod(item)} - /> - )} - /> -); + isLoadingMethods, + isLoadingWebView +}: OwnProps) => { + const selectedPaymentMethodId = useIOSelector( + selectPaymentOnboardingSelectedMethod + ); + const isMethodLoading = (itemId: string) => + isLoadingWebView && itemId === selectedPaymentMethodId; + const ListFooter = () => renderListFooter(isLoadingMethods); + + return ( + item.id} + ListFooterComponent={} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + onSelectPaymentMethod(item)} + /> + )} + /> + ); +}; const renderListFooter = (isLoading?: boolean) => { if (isLoading) { diff --git a/ts/features/walletV3/common/components/WalletPaymentMethodItemSkeleton.tsx b/ts/features/payments/onboarding/components/WalletPaymentMethodItemSkeleton.tsx similarity index 100% rename from ts/features/walletV3/common/components/WalletPaymentMethodItemSkeleton.tsx rename to ts/features/payments/onboarding/components/WalletPaymentMethodItemSkeleton.tsx diff --git a/ts/features/payments/onboarding/hooks/useWalletOnboardingWebView.tsx b/ts/features/payments/onboarding/hooks/useWalletOnboardingWebView.tsx new file mode 100644 index 00000000000..db7cdaf2993 --- /dev/null +++ b/ts/features/payments/onboarding/hooks/useWalletOnboardingWebView.tsx @@ -0,0 +1,116 @@ +import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as E from "fp-ts/lib/Either"; +import * as TE from "fp-ts/lib/TaskEither"; +import { pipe } from "fp-ts/lib/function"; +import * as React from "react"; +import URLParse from "url-parse"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { selectPaymentOnboardingRequestResult } from "../store/selectors"; +import { paymentsStartOnboardingAction } from "../store/actions"; +import { + WalletOnboardingOutcome, + WalletOnboardingOutcomeEnum +} from "../types/OnboardingOutcomeEnum"; +import { ONBOARDING_CALLBACK_URL_SCHEMA } from "../utils"; + +type WalletOnboardingWebViewProps = { + onOnboardingOutcome: ( + outcome: WalletOnboardingOutcome, + walletId?: string + ) => void; +}; + +export type WalletOnboardingWebView = { + isLoading: boolean; + isError: boolean; + isPendingOnboarding: boolean; + startOnboarding: (paymentMethodId: string) => void; +}; + +/** + * This hook handles the onboarding webview flow and returns a function to start the onboarding + * @param onOnboardingOutcome callback called when the onboarding flow is completed + */ +export const useWalletOnboardingWebView = ({ + onOnboardingOutcome +}: WalletOnboardingWebViewProps): WalletOnboardingWebView => { + const dispatch = useIODispatch(); + + const onboardingUrlPot = useIOSelector(selectPaymentOnboardingRequestResult); + + const [isPendingOnboarding, setIsPendingOnboarding] = + React.useState(false); + const isLoading = pot.isLoading(onboardingUrlPot); + const isError = pot.isError(onboardingUrlPot); + + const handleOnboardingResult = React.useCallback( + (resultUrl: string) => { + const url = new URLParse(resultUrl, true); + + const outcome = pipe( + url.query.outcome, + WalletOnboardingOutcome.decode, + E.getOrElse(() => WalletOnboardingOutcomeEnum.GENERIC_ERROR) + ); + + onOnboardingOutcome(outcome, url.query.walletId); + }, + [onOnboardingOutcome] + ); + + React.useEffect(() => { + if (isPendingOnboarding) { + return; + } + + void pipe( + onboardingUrlPot, + pot.toOption, + TE.fromOption(() => undefined), + TE.chain(({ redirectUrl }) => + TE.tryCatch( + () => { + setIsPendingOnboarding(true); + return openAuthenticationSession( + redirectUrl, + ONBOARDING_CALLBACK_URL_SCHEMA + ); + }, + () => { + onOnboardingOutcome(WalletOnboardingOutcomeEnum.CANCELED_BY_USER); + } + ) + ), + TE.map(handleOnboardingResult) + )(); + }, [ + isError, + isLoading, + isPendingOnboarding, + onboardingUrlPot, + handleOnboardingResult, + onOnboardingOutcome, + dispatch + ]); + + React.useEffect( + () => () => { + setIsPendingOnboarding(false); + dispatch(paymentsStartOnboardingAction.cancel()); + }, + [dispatch] + ); + + const startOnboarding = (paymentMethodId: string) => { + setIsPendingOnboarding(false); + dispatch(paymentsStartOnboardingAction.request({ paymentMethodId })); + }; + + return { + startOnboarding, + isLoading, + isError, + isPendingOnboarding + }; +}; diff --git a/ts/features/payments/onboarding/navigation/navigator.tsx b/ts/features/payments/onboarding/navigation/navigator.tsx new file mode 100644 index 00000000000..a752dc9cb02 --- /dev/null +++ b/ts/features/payments/onboarding/navigation/navigator.tsx @@ -0,0 +1,44 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import { PaymentsOnboardingFeedbackScreen } from "../screens/PaymentsOnboardingFeedbackScreen"; +import { PaymentsOnboardingSelectMethodScreen } from "../screens/PaymentsOnboardingSelectMethodScreen"; +import { PaymentsOnboardingParamsList } from "./params"; +import { PaymentsOnboardingRoutes } from "./routes"; + +const Stack = createStackNavigator(); + +export const PaymentsOnboardingNavigator = () => ( + + + + +); + +export type PaymentsOnboardingStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type PaymentsOnboardingStackNavigation = + PaymentsOnboardingStackNavigationProp< + PaymentsOnboardingParamsList, + keyof PaymentsOnboardingParamsList + >; diff --git a/ts/features/payments/onboarding/navigation/params.ts b/ts/features/payments/onboarding/navigation/params.ts new file mode 100644 index 00000000000..674aae1fb09 --- /dev/null +++ b/ts/features/payments/onboarding/navigation/params.ts @@ -0,0 +1,8 @@ +import { PaymentsOnboardingFeedbackScreenParams } from "../screens/PaymentsOnboardingFeedbackScreen"; +import { PaymentsOnboardingRoutes } from "./routes"; + +export type PaymentsOnboardingParamsList = { + [PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR]: undefined; + [PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_RESULT_FEEDBACK]: PaymentsOnboardingFeedbackScreenParams; + [PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD]: undefined; +}; diff --git a/ts/features/payments/onboarding/navigation/routes.ts b/ts/features/payments/onboarding/navigation/routes.ts new file mode 100644 index 00000000000..15f672d110e --- /dev/null +++ b/ts/features/payments/onboarding/navigation/routes.ts @@ -0,0 +1,5 @@ +export const PaymentsOnboardingRoutes = { + PAYMENT_ONBOARDING_NAVIGATOR: "PAYMENT_ONBOARDING_NAVIGATOR", + PAYMENT_ONBOARDING_SELECT_METHOD: "PAYMENT_ONBOARDING_SELECT_METHOD", + PAYMENT_ONBOARDING_RESULT_FEEDBACK: "PAYMENT_ONBOARDING_RESULT_FEEDBACK" +} as const; diff --git a/ts/features/walletV3/onboarding/saga/handleGetPaymentMethods.ts b/ts/features/payments/onboarding/saga/handleGetPaymentMethods.ts similarity index 77% rename from ts/features/walletV3/onboarding/saga/handleGetPaymentMethods.ts rename to ts/features/payments/onboarding/saga/handleGetPaymentMethods.ts index c44bbb2875d..dd0f1b62d2e 100644 --- a/ts/features/walletV3/onboarding/saga/handleGetPaymentMethods.ts +++ b/ts/features/payments/onboarding/saga/handleGetPaymentMethods.ts @@ -2,7 +2,7 @@ import { ActionType } from "typesafe-actions"; import { call, put } from "typed-redux-saga/macro"; import * as E from "fp-ts/lib/Either"; import { SagaCallReturnType } from "../../../../types/utils"; -import { walletGetPaymentMethods } from "../store/actions"; +import { paymentsOnboardingGetMethodsAction } from "../store/actions"; import { readablePrivacyReport } from "../../../../utils/reporters"; import { getGenericError, getNetworkError } from "../../../../utils/errors"; import { WalletClient } from "../../common/api/client"; @@ -15,7 +15,7 @@ import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; */ export function* handleGetPaymentMethods( getPaymentMethods: WalletClient["getAllPaymentMethods"], - action: ActionType<(typeof walletGetPaymentMethods)["request"]> + action: ActionType<(typeof paymentsOnboardingGetMethodsAction)["request"]> ) { const getPaymentMethodsRequest = getPaymentMethods({}); try { @@ -28,13 +28,15 @@ export function* handleGetPaymentMethods( if (getPaymentMethodsResult.right.status === 200) { // handled success yield* put( - walletGetPaymentMethods.success(getPaymentMethodsResult.right.value) + paymentsOnboardingGetMethodsAction.success( + getPaymentMethodsResult.right.value + ) ); return; } // not handled error codes yield* put( - walletGetPaymentMethods.failure({ + paymentsOnboardingGetMethodsAction.failure({ ...getGenericError( new Error( `response status code ${getPaymentMethodsResult.right.status}` @@ -45,7 +47,7 @@ export function* handleGetPaymentMethods( } else { // cannot decode response yield* put( - walletGetPaymentMethods.failure({ + paymentsOnboardingGetMethodsAction.failure({ ...getGenericError( new Error(readablePrivacyReport(getPaymentMethodsResult.left)) ) @@ -53,6 +55,8 @@ export function* handleGetPaymentMethods( ); } } catch (e) { - yield* put(walletGetPaymentMethods.failure({ ...getNetworkError(e) })); + yield* put( + paymentsOnboardingGetMethodsAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/onboarding/saga/handleStartWalletOnboarding.ts b/ts/features/payments/onboarding/saga/handleStartWalletOnboarding.ts similarity index 77% rename from ts/features/walletV3/onboarding/saga/handleStartWalletOnboarding.ts rename to ts/features/payments/onboarding/saga/handleStartWalletOnboarding.ts index ec5df9e6383..bdb35959ed9 100644 --- a/ts/features/walletV3/onboarding/saga/handleStartWalletOnboarding.ts +++ b/ts/features/payments/onboarding/saga/handleStartWalletOnboarding.ts @@ -2,11 +2,10 @@ import { call, put } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; import * as E from "fp-ts/lib/Either"; import { SagaCallReturnType } from "../../../../types/utils"; -import { walletStartOnboarding } from "../store/actions"; +import { paymentsStartOnboardingAction } from "../store/actions"; import { readablePrivacyReport } from "../../../../utils/reporters"; import { getGenericError, getNetworkError } from "../../../../utils/errors"; import { WalletClient } from "../../common/api/client"; -import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; /** @@ -16,13 +15,13 @@ import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; */ export function* handleStartWalletOnboarding( startOnboarding: WalletClient["createWallet"], - action: ActionType<(typeof walletStartOnboarding)["request"]> + action: ActionType<(typeof paymentsStartOnboardingAction)["request"]> ) { try { const { paymentMethodId } = action.payload; const startOnboardingRequest = startOnboarding({ body: { - services: [ServiceNameEnum.PAGOPA], + applications: ["PAGOPA"], useDiagnosticTracing: true, paymentMethodId } @@ -36,13 +35,15 @@ export function* handleStartWalletOnboarding( if (startOnboardingResult.right.status === 201) { // handled success yield* put( - walletStartOnboarding.success(startOnboardingResult.right.value) + paymentsStartOnboardingAction.success( + startOnboardingResult.right.value + ) ); return; } // not handled error codes yield* put( - walletStartOnboarding.failure({ + paymentsStartOnboardingAction.failure({ ...getGenericError( new Error( `response status code ${startOnboardingResult.right.status}` @@ -53,7 +54,7 @@ export function* handleStartWalletOnboarding( } else { // cannot decode response yield* put( - walletStartOnboarding.failure({ + paymentsStartOnboardingAction.failure({ ...getGenericError( new Error(readablePrivacyReport(startOnboardingResult.left)) ) @@ -61,6 +62,8 @@ export function* handleStartWalletOnboarding( ); } } catch (e) { - yield* put(walletStartOnboarding.failure({ ...getNetworkError(e) })); + yield* put( + paymentsStartOnboardingAction.failure({ ...getNetworkError(e) }) + ); } } diff --git a/ts/features/walletV3/onboarding/saga/index.ts b/ts/features/payments/onboarding/saga/index.ts similarity index 78% rename from ts/features/walletV3/onboarding/saga/index.ts rename to ts/features/payments/onboarding/saga/index.ts index ef577067eca..d3b24d1ef25 100644 --- a/ts/features/walletV3/onboarding/saga/index.ts +++ b/ts/features/payments/onboarding/saga/index.ts @@ -3,8 +3,8 @@ import { takeLatest } from "typed-redux-saga/macro"; import { WalletClient } from "../../common/api/client"; import { - walletGetPaymentMethods, - walletStartOnboarding + paymentsOnboardingGetMethodsAction, + paymentsStartOnboardingAction } from "../store/actions"; import { handleStartWalletOnboarding } from "./handleStartWalletOnboarding"; import { handleGetPaymentMethods } from "./handleGetPaymentMethods"; @@ -13,19 +13,19 @@ import { handleGetPaymentMethods } from "./handleGetPaymentMethods"; * Handle Wallet onboarding requests * @param bearerToken */ -export function* watchWalletOnboardingSaga( +export function* watchPaymentsOnboardingSaga( walletClient: WalletClient ): SagaIterator { // handle the request of starting wallet onboarding yield* takeLatest( - walletStartOnboarding.request, + paymentsStartOnboardingAction.request, handleStartWalletOnboarding, walletClient.createWallet ); // handle the request of get list of payment methods available into onboarding yield* takeLatest( - walletGetPaymentMethods.request, + paymentsOnboardingGetMethodsAction.request, handleGetPaymentMethods, walletClient.getAllPaymentMethods ); diff --git a/ts/features/payments/onboarding/screens/PaymentsOnboardingFeedbackScreen.tsx b/ts/features/payments/onboarding/screens/PaymentsOnboardingFeedbackScreen.tsx new file mode 100644 index 00000000000..47afabd45c5 --- /dev/null +++ b/ts/features/payments/onboarding/screens/PaymentsOnboardingFeedbackScreen.tsx @@ -0,0 +1,121 @@ +import { IOPictograms } from "@pagopa/io-app-design-system"; +import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; +import * as React from "react"; +import { View } from "react-native"; +import { IOStyles } from "../../../../components/core/variables/IOStyles"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import I18n from "../../../../i18n"; +import { + AppParamsList, + IOStackNavigationProp +} from "../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../navigation/routes"; +import { openWebUrl } from "../../../../utils/url"; +import { PaymentsMethodDetailsRoutes } from "../../details/navigation/routes"; +import { PaymentsOnboardingParamsList } from "../navigation/params"; +import { + WalletOnboardingOutcome, + WalletOnboardingOutcomeEnum +} from "../types/OnboardingOutcomeEnum"; +import { ONBOARDING_FAQ_ENABLE_3DS } from "../utils"; + +export type PaymentsOnboardingFeedbackScreenParams = { + outcome: WalletOnboardingOutcome; + walletId?: string; +}; + +type PaymentsOnboardingFeedbackScreenRouteProps = RouteProp< + PaymentsOnboardingParamsList, + "PAYMENT_ONBOARDING_RESULT_FEEDBACK" +>; + +export const pictogramByOutcome: Record = + { + [WalletOnboardingOutcomeEnum.SUCCESS]: "success", + [WalletOnboardingOutcomeEnum.GENERIC_ERROR]: "umbrellaNew", + [WalletOnboardingOutcomeEnum.AUTH_ERROR]: "accessDenied", + [WalletOnboardingOutcomeEnum.TIMEOUT]: "time", + [WalletOnboardingOutcomeEnum.CANCELED_BY_USER]: "trash", + [WalletOnboardingOutcomeEnum.INVALID_SESSION]: "umbrellaNew", + [WalletOnboardingOutcomeEnum.ALREADY_ONBOARDED]: "success", + [WalletOnboardingOutcomeEnum.BPAY_NOT_FOUND]: "attention" + }; + +const PaymentsOnboardingFeedbackScreen = () => { + const navigation = useNavigation>(); + const route = useRoute(); + const { outcome, walletId } = route.params; + + const outcomeEnumKey = Object.keys(WalletOnboardingOutcomeEnum)[ + Object.values(WalletOnboardingOutcomeEnum).indexOf(outcome) + ] as keyof typeof WalletOnboardingOutcomeEnum; + + const handleContinueButton = () => { + navigation.popToTop(); + if (outcome === WalletOnboardingOutcomeEnum.SUCCESS && walletId) { + navigation.reset({ + index: 1, + routes: [ + { + name: ROUTES.MAIN, + params: { + screen: ROUTES.WALLET_HOME, + params: { + newMethodAdded: true + } + } + }, + { + name: PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_NAVIGATOR, + params: { + screen: PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_SCREEN, + params: { + walletId + } + } + } + ] + }); + } else { + navigation.popToTop(); + } + }; + + const renderSecondaryAction = () => { + switch (outcome) { + case WalletOnboardingOutcomeEnum.AUTH_ERROR: + return { + label: I18n.t(`wallet.onboarding.outcome.AUTH_ERROR.secondaryAction`), + accessibilityLabel: I18n.t( + `wallet.onboarding.outcome.AUTH_ERROR.secondaryAction` + ), + onPress: () => openWebUrl(ONBOARDING_FAQ_ENABLE_3DS) + }; + } + return undefined; + }; + + return ( + + + + ); +}; + +export { PaymentsOnboardingFeedbackScreen }; diff --git a/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx b/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx new file mode 100644 index 00000000000..37da80d9db9 --- /dev/null +++ b/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx @@ -0,0 +1,95 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import * as React from "react"; +import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; +import { PaymentMethodStatusEnum } from "../../../../../definitions/pagopa/walletv3/PaymentMethodStatus"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; +import WalletOnboardingPaymentMethodsList from "../components/WalletOnboardingPaymentMethodsList"; +import { useWalletOnboardingWebView } from "../hooks/useWalletOnboardingWebView"; +import { PaymentsOnboardingRoutes } from "../navigation/routes"; +import { paymentsOnboardingGetMethodsAction } from "../store/actions"; +import { selectPaymentOnboardingMethods } from "../store/selectors"; + +const PaymentsOnboardingSelectMethodScreen = () => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + + const paymentMethodsPot = useIOSelector(selectPaymentOnboardingMethods); + const isLoadingPaymentMethods = pot.isLoading(paymentMethodsPot); + + const availablePaymentMethods = pipe( + pot.getOrElse( + pot.map(paymentMethodsPot, el => el.paymentMethods), + null + ), + O.fromNullable, + O.map(el => el.filter(el => el.status === PaymentMethodStatusEnum.ENABLED)), + O.getOrElseW(() => []) + ); + + const { startOnboarding, isLoading, isPendingOnboarding } = + useWalletOnboardingWebView({ + onOnboardingOutcome: (outcome, walletId) => { + navigation.replace( + PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR, + { + screen: PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_RESULT_FEEDBACK, + params: { + outcome, + walletId + } + } + ); + } + }); + + useOnFirstRender(() => { + dispatch(paymentsOnboardingGetMethodsAction.request()); + }); + + const handleSelectedPaymentMethod = ( + selectedPaymentMethod: PaymentMethodResponse + ) => { + startOnboarding(selectedPaymentMethod.id); + }; + + if (pot.isError(paymentMethodsPot)) { + return ( + dispatch(paymentsOnboardingGetMethodsAction.request()) + }} + /> + ); + } + return ( + + + + ); +}; + +export { PaymentsOnboardingSelectMethodScreen }; diff --git a/ts/features/payments/onboarding/store/actions/index.ts b/ts/features/payments/onboarding/store/actions/index.ts new file mode 100644 index 00000000000..7d07e4f83ed --- /dev/null +++ b/ts/features/payments/onboarding/store/actions/index.ts @@ -0,0 +1,22 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../../utils/errors"; +import { WalletCreateResponse } from "../../../../../../definitions/pagopa/walletv3/WalletCreateResponse"; +import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; + +export const paymentsOnboardingGetMethodsAction = createAsyncAction( + "PAYMENTS_GET_ONBOARDING_METHODS_REQUEST", + "PAYMENTS_GET_ONBOARDING_METHODS_SUCCESS", + "PAYMENTS_GET_ONBOARDING_METHODS_FAILURE", + "PAYMENTS_GET_ONBOARDING_METHODS_CANCEL" +)(); + +export const paymentsStartOnboardingAction = createAsyncAction( + "PAYMENTS_START_ONBOARDING_REQUEST", + "PAYMENTS_START_ONBOARDING_SUCCESS", + "PAYMENTS_START_ONBOARDING_FAILURE", + "PAYMENTS_START_ONBOARDING_CANCEL" +)<{ paymentMethodId: string }, WalletCreateResponse, NetworkError, void>(); + +export type PaymentsOnboardingActions = + | ActionType + | ActionType; diff --git a/ts/features/payments/onboarding/store/reducers/index.ts b/ts/features/payments/onboarding/store/reducers/index.ts new file mode 100644 index 00000000000..007658c8ae4 --- /dev/null +++ b/ts/features/payments/onboarding/store/reducers/index.ts @@ -0,0 +1,78 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; +import { WalletCreateResponse } from "../../../../../../definitions/pagopa/walletv3/WalletCreateResponse"; +import { Action } from "../../../../../store/actions/types"; +import { NetworkError } from "../../../../../utils/errors"; + +import { + paymentsOnboardingGetMethodsAction, + paymentsStartOnboardingAction +} from "../actions"; + +export type PaymentsOnboardingState = { + result: pot.Pot; + paymentMethods: pot.Pot; + selectedPaymentMethodId?: string; +}; + +const INITIAL_STATE: PaymentsOnboardingState = { + result: pot.none, + paymentMethods: pot.noneLoading, + selectedPaymentMethodId: undefined +}; + +const reducer = ( + state: PaymentsOnboardingState = INITIAL_STATE, + action: Action +): PaymentsOnboardingState => { + switch (action.type) { + // START ONBOARDING ACTIONS + case getType(paymentsStartOnboardingAction.request): + return { + ...state, + selectedPaymentMethodId: action.payload.paymentMethodId, + result: pot.toLoading(pot.none) + }; + case getType(paymentsStartOnboardingAction.success): + return { + ...state, + result: pot.some(action.payload as WalletCreateResponse) + }; + case getType(paymentsStartOnboardingAction.failure): + return { + ...state, + result: pot.toError(state.result, action.payload) + }; + case getType(paymentsStartOnboardingAction.cancel): + return { + ...state, + selectedPaymentMethodId: undefined, + result: pot.none + }; + // GET ONBOARDABLE PAYMENT METHODS LIST + case getType(paymentsOnboardingGetMethodsAction.request): + return { + ...state, + paymentMethods: pot.toLoading(pot.none) + }; + case getType(paymentsOnboardingGetMethodsAction.success): + return { + ...state, + paymentMethods: pot.some(action.payload) + }; + case getType(paymentsOnboardingGetMethodsAction.failure): + return { + ...state, + paymentMethods: pot.toError(state.paymentMethods, action.payload) + }; + case getType(paymentsOnboardingGetMethodsAction.cancel): + return { + ...state, + paymentMethods: pot.none + }; + } + return state; +}; + +export default reducer; diff --git a/ts/features/payments/onboarding/store/selectors/index.ts b/ts/features/payments/onboarding/store/selectors/index.ts new file mode 100644 index 00000000000..3f019525f91 --- /dev/null +++ b/ts/features/payments/onboarding/store/selectors/index.ts @@ -0,0 +1,13 @@ +import { GlobalState } from "../../../../../store/reducers/types"; + +const walletOnboardingSelector = (state: GlobalState) => + state.features.payments.onboarding; + +export const selectPaymentOnboardingRequestResult = (state: GlobalState) => + walletOnboardingSelector(state).result; + +export const selectPaymentOnboardingMethods = (state: GlobalState) => + walletOnboardingSelector(state).paymentMethods; + +export const selectPaymentOnboardingSelectedMethod = (state: GlobalState) => + walletOnboardingSelector(state).selectedPaymentMethodId; diff --git a/ts/features/payments/onboarding/types/OnboardingOutcomeEnum.ts b/ts/features/payments/onboarding/types/OnboardingOutcomeEnum.ts new file mode 100644 index 00000000000..12bcf64190c --- /dev/null +++ b/ts/features/payments/onboarding/types/OnboardingOutcomeEnum.ts @@ -0,0 +1,19 @@ +import { enumType } from "@pagopa/ts-commons/lib/types"; +import * as t from "io-ts"; + +export enum WalletOnboardingOutcomeEnum { + SUCCESS = "0", + GENERIC_ERROR = "1", + AUTH_ERROR = "2", + TIMEOUT = "4", + CANCELED_BY_USER = "8", + INVALID_SESSION = "14", + ALREADY_ONBOARDED = "15", + BPAY_NOT_FOUND = "16" +} + +export type WalletOnboardingOutcome = t.TypeOf; +export const WalletOnboardingOutcome = enumType( + WalletOnboardingOutcomeEnum, + "WalletOnboardingOutcome" +); diff --git a/ts/features/payments/onboarding/utils/index.ts b/ts/features/payments/onboarding/utils/index.ts new file mode 100644 index 00000000000..b5e3a46516d --- /dev/null +++ b/ts/features/payments/onboarding/utils/index.ts @@ -0,0 +1,3 @@ +export const ONBOARDING_FAQ_ENABLE_3DS = "https://io.italia.it/faq/#n3_3"; +export const ONBOARDING_CALLBACK_URL_SCHEMA = "iowallet"; +export const ONBOARDING_OUTCOME_PATH = "/wallets/outcomes"; diff --git a/ts/features/walletV3/transaction/components/WalletTransactionDetailsList.tsx b/ts/features/payments/transaction/components/WalletTransactionDetailsList.tsx similarity index 100% rename from ts/features/walletV3/transaction/components/WalletTransactionDetailsList.tsx rename to ts/features/payments/transaction/components/WalletTransactionDetailsList.tsx diff --git a/ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx b/ts/features/payments/transaction/components/WalletTransactionHeadingSection.tsx similarity index 89% rename from ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx rename to ts/features/payments/transaction/components/WalletTransactionHeadingSection.tsx index a9cf9459284..45a6a47e237 100644 --- a/ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx +++ b/ts/features/payments/transaction/components/WalletTransactionHeadingSection.tsx @@ -1,19 +1,16 @@ +import { Body, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; -import Placeholder from "rn-placeholder"; import React from "react"; import { View } from "react-native"; -import { Body, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; -import { Psp, Transaction } from "../../../../types/pagopa"; +import Placeholder from "rn-placeholder"; import { Dettaglio } from "../../../../../definitions/pagopa/Dettaglio"; -import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import I18n from "../../../../i18n"; -import { - WalletTransactionRoutes, - WalletTransactionStackNavigation -} from "../navigation/navigator"; - -import { WalletTransactionTotalAmount } from "./WalletTransactionTotalAmount"; +import { Psp, Transaction } from "../../../../types/pagopa"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { PaymentsTransactionStackNavigation } from "../navigation/navigator"; +import { PaymentsTransactionRoutes } from "../navigation/routes"; import { WalletTransactionDetailsList } from "./WalletTransactionDetailsList"; +import { WalletTransactionTotalAmount } from "./WalletTransactionTotalAmount"; type Props = { transaction?: Transaction; @@ -26,12 +23,12 @@ export const WalletTransactionHeadingSection = ({ psp, isLoading }: Props) => { - const navigation = useNavigation(); + const navigation = useNavigation(); const handlePressTransactionDetails = (operationDetails: Dettaglio) => { if (transaction) { navigation.navigate( - WalletTransactionRoutes.WALLET_TRANSACTION_OPERATION_DETAILS, + PaymentsTransactionRoutes.PAYMENT_TRANSACTION_OPERATION_DETAILS, { operationDetails, operationSubject: transaction.description, diff --git a/ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx b/ts/features/payments/transaction/components/WalletTransactionInfoSection.tsx similarity index 91% rename from ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx rename to ts/features/payments/transaction/components/WalletTransactionInfoSection.tsx index b437782e97a..4d24aa3628f 100644 --- a/ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx +++ b/ts/features/payments/transaction/components/WalletTransactionInfoSection.tsx @@ -72,9 +72,6 @@ const WalletTransactionInfoSection = ({ {psp?.businessName && ( <> @@ -82,9 +79,6 @@ const WalletTransactionInfoSection = ({ )} @@ -93,9 +87,9 @@ const WalletTransactionInfoSection = ({ onPress={() => clipboardSetStringWithFeedback(transaction.id.toString()) } - accessibilityLabel={I18n.t( + accessibilityLabel={`${I18n.t( "transaction.details.info.transactionId" - )} + )}: ${transaction.id.toString()}`} label={I18n.t("transaction.details.info.transactionId")} value={transaction.id.toString()} /> diff --git a/ts/features/walletV3/transaction/components/WalletTransactionTotalAmount.tsx b/ts/features/payments/transaction/components/WalletTransactionTotalAmount.tsx similarity index 100% rename from ts/features/walletV3/transaction/components/WalletTransactionTotalAmount.tsx rename to ts/features/payments/transaction/components/WalletTransactionTotalAmount.tsx diff --git a/ts/features/payments/transaction/navigation/navigator.tsx b/ts/features/payments/transaction/navigation/navigator.tsx new file mode 100644 index 00000000000..c91c24e4b1a --- /dev/null +++ b/ts/features/payments/transaction/navigation/navigator.tsx @@ -0,0 +1,42 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import { PaymentsTransactionDetailsScreen } from "../screens/PaymentsTransactionDetailsScreen"; +import WalletTransactionOperationDetailsScreen from "../screens/PaymentsTransactionOperationDetails"; +import { PaymentsTransactionParamsList } from "./params"; +import { PaymentsTransactionRoutes } from "./routes"; + +const Stack = createStackNavigator(); + +export const PaymentsTransactionNavigator = () => ( + + + + +); + +export type PaymentsTransactionStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type PaymentsTransactionStackNavigation = + PaymentsTransactionStackNavigationProp< + PaymentsTransactionParamsList, + keyof PaymentsTransactionParamsList + >; diff --git a/ts/features/payments/transaction/navigation/params.ts b/ts/features/payments/transaction/navigation/params.ts new file mode 100644 index 00000000000..2449c47b441 --- /dev/null +++ b/ts/features/payments/transaction/navigation/params.ts @@ -0,0 +1,9 @@ +import { PaymentsTransactionDetailsScreenParams } from "../screens/PaymentsTransactionDetailsScreen"; +import { PaymentsTransactionOperationDetailsScreenParams } from "../screens/PaymentsTransactionOperationDetails"; +import { PaymentsTransactionRoutes } from "./routes"; + +export type PaymentsTransactionParamsList = { + [PaymentsTransactionRoutes.PAYMENT_TRANSACTION_NAVIGATOR]: undefined; + [PaymentsTransactionRoutes.PAYMENT_TRANSACTION_DETAILS]: PaymentsTransactionDetailsScreenParams; + [PaymentsTransactionRoutes.PAYMENT_TRANSACTION_OPERATION_DETAILS]: PaymentsTransactionOperationDetailsScreenParams; +}; diff --git a/ts/features/payments/transaction/navigation/routes.ts b/ts/features/payments/transaction/navigation/routes.ts new file mode 100644 index 00000000000..d4b113de118 --- /dev/null +++ b/ts/features/payments/transaction/navigation/routes.ts @@ -0,0 +1,5 @@ +export const PaymentsTransactionRoutes = { + PAYMENT_TRANSACTION_NAVIGATOR: "PAYMENT_TRANSACTION_NAVIGATOR", + PAYMENT_TRANSACTION_DETAILS: "PAYMENT_TRANSACTION_DETAILS", + PAYMENT_TRANSACTION_OPERATION_DETAILS: "PAYMENT_TRANSACTION_OPERATION_DETAILS" +} as const; diff --git a/ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts b/ts/features/payments/transaction/saga/handleGetTransactionDetails.ts similarity index 82% rename from ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts rename to ts/features/payments/transaction/saga/handleGetTransactionDetails.ts index 1d5408275d8..8ded1471ddf 100644 --- a/ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts +++ b/ts/features/payments/transaction/saga/handleGetTransactionDetails.ts @@ -1,7 +1,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { put, select } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; -import { walletTransactionDetailsGet } from "../store/actions"; +import { getPaymentsTransactionDetailsAction } from "../store/actions"; import { getTransactions } from "../../../../store/reducers/wallet/transactions"; import { getGenericError } from "../../../../utils/errors"; @@ -13,7 +13,7 @@ import { getGenericError } from "../../../../utils/errors"; */ export function* handleGetTransactionDetails( _getTransactionDetails: any, // TODO: Replace with the real type when the BIZ Event API will be available - action: ActionType<(typeof walletTransactionDetailsGet)["request"]> + action: ActionType<(typeof getPaymentsTransactionDetailsAction)["request"]> ) { // TODO: Add the whole logic here to call the BIZ Event API as soon as it will be available and replace the following code const transactions = yield* select(getTransactions); @@ -23,11 +23,11 @@ export function* handleGetTransactionDetails( ) ); if (transactionDetails) { - yield* put(walletTransactionDetailsGet.success(transactionDetails)); + yield* put(getPaymentsTransactionDetailsAction.success(transactionDetails)); return; } yield* put( - walletTransactionDetailsGet.failure({ + getPaymentsTransactionDetailsAction.failure({ ...getGenericError( new Error( `Transaction details not found for transaction id ${action.payload.transactionId}` diff --git a/ts/features/walletV3/transaction/saga/index.ts b/ts/features/payments/transaction/saga/index.ts similarity index 79% rename from ts/features/walletV3/transaction/saga/index.ts rename to ts/features/payments/transaction/saga/index.ts index a9d60b20539..5a31a99cde5 100644 --- a/ts/features/walletV3/transaction/saga/index.ts +++ b/ts/features/payments/transaction/saga/index.ts @@ -2,19 +2,19 @@ import { SagaIterator } from "redux-saga"; import { takeLatest } from "typed-redux-saga/macro"; import { WalletClient } from "../../common/api/client"; -import { walletTransactionDetailsGet } from "../store/actions"; +import { getPaymentsTransactionDetailsAction } from "../store/actions"; import { handleGetTransactionDetails } from "./handleGetTransactionDetails"; /** * Handle Wallet transaction requests * @param bearerToken */ -export function* watchWalletTransactionSaga( +export function* watchPaymentsTransactionSaga( walletClient: WalletClient ): SagaIterator { // TODO: Connect the saga code here to the BIZ Event API as asoon as it will be available (https://pagopa.atlassian.net/browse/IOBP-440) yield* takeLatest( - walletTransactionDetailsGet.request, + getPaymentsTransactionDetailsAction.request, handleGetTransactionDetails, walletClient.getWalletById // TODO: Add the get transaction details API call here when BIZ Event API will be available ); diff --git a/ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx b/ts/features/payments/transaction/screens/PaymentsTransactionDetailsScreen.tsx similarity index 80% rename from ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx rename to ts/features/payments/transaction/screens/PaymentsTransactionDetailsScreen.tsx index bbf9c922b7b..721dd3dbf01 100644 --- a/ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx +++ b/ts/features/payments/transaction/screens/PaymentsTransactionDetailsScreen.tsx @@ -1,30 +1,29 @@ -import * as React from "react"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { Dimensions, StyleSheet, View } from "react-native"; import { IOColors } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; import { RouteProp, useRoute } from "@react-navigation/native"; - -import { WalletTransactionParamsList } from "../navigation/navigator"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import * as React from "react"; +import { Dimensions, StyleSheet, View } from "react-native"; import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; -import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { walletTransactionDetailsGet } from "../store/actions"; -import { walletTransactionDetailsPotSelector } from "../store"; +import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; +import I18n from "../../../../i18n"; import { fetchPsp } from "../../../../store/actions/wallet/transactions"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { Psp } from "../../../../types/pagopa"; -import WalletTransactionInfoSection from "../components/WalletTransactionInfoSection"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; import { WalletTransactionHeadingSection } from "../components/WalletTransactionHeadingSection"; -import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; -import I18n from "../../../../i18n"; +import WalletTransactionInfoSection from "../components/WalletTransactionInfoSection"; +import { PaymentsTransactionParamsList } from "../navigation/params"; +import { getPaymentsTransactionDetailsAction } from "../store/actions"; +import { walletTransactionDetailsPotSelector } from "../store/selectors"; -export type WalletTransactionDetailsScreenParams = { +export type PaymentsTransactionDetailsScreenParams = { transactionId: number; }; -export type WalletTransactionDetailsScreenProps = RouteProp< - WalletTransactionParamsList, - "WALLET_TRANSACTION_DETAILS" +export type PaymentsTransactionDetailsScreenProps = RouteProp< + PaymentsTransactionParamsList, + "PAYMENT_TRANSACTION_DETAILS" >; const windowHeight = Dimensions.get("window").height; @@ -45,10 +44,10 @@ const styles = StyleSheet.create({ } }); -const WalletTransactionDetailsScreen = () => { +const PaymentsTransactionDetailsScreen = () => { const [transactionPsp, setTransactionPsp] = React.useState(); const dispatch = useIODispatch(); - const route = useRoute(); + const route = useRoute(); const { transactionId } = route.params; const transactionDetailsPot = useIOSelector( walletTransactionDetailsPotSelector @@ -58,7 +57,7 @@ const WalletTransactionDetailsScreen = () => { const transactionDetails = pot.toUndefined(transactionDetailsPot); useOnFirstRender(() => { - dispatch(walletTransactionDetailsGet.request({ transactionId })); + dispatch(getPaymentsTransactionDetailsAction.request({ transactionId })); }); React.useEffect(() => { @@ -76,7 +75,9 @@ const WalletTransactionDetailsScreen = () => { return ( { ); }; -export default WalletTransactionDetailsScreen; +export { PaymentsTransactionDetailsScreen }; diff --git a/ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx b/ts/features/payments/transaction/screens/PaymentsTransactionOperationDetails.tsx similarity index 92% rename from ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx rename to ts/features/payments/transaction/screens/PaymentsTransactionOperationDetails.tsx index 92a1db7ab6d..9bb413b92c2 100644 --- a/ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx +++ b/ts/features/payments/transaction/screens/PaymentsTransactionOperationDetails.tsx @@ -7,8 +7,7 @@ import { ListItemInfo } from "@pagopa/io-app-design-system"; import { RouteProp, useRoute } from "@react-navigation/native"; - -import { WalletTransactionParamsList } from "../navigation/navigator"; +import { PaymentsTransactionParamsList } from "../navigation/params"; import { Dettaglio } from "../../../../../definitions/pagopa/Dettaglio"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { cleanTransactionDescription } from "../../../../utils/payment"; @@ -22,15 +21,15 @@ const styles = StyleSheet.create({ } }); -export type WalletTransactionOperationDetailsScreenParams = { +export type PaymentsTransactionOperationDetailsScreenParams = { operationName: string; operationSubject: string; operationDetails: Dettaglio; }; export type WalletTransactionOperationDetailsScreenProps = RouteProp< - WalletTransactionParamsList, - "WALLET_TRANSACTION_OPERATION_DETAILS" + PaymentsTransactionParamsList, + "PAYMENT_TRANSACTION_OPERATION_DETAILS" >; const WalletTransactionOperationDetailsScreen = () => { @@ -58,7 +57,9 @@ const WalletTransactionOperationDetailsScreen = () => { return ( {operationDetails.importo && ( diff --git a/ts/features/payments/transaction/store/actions/index.ts b/ts/features/payments/transaction/store/actions/index.ts new file mode 100644 index 00000000000..ec338ef40c5 --- /dev/null +++ b/ts/features/payments/transaction/store/actions/index.ts @@ -0,0 +1,14 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../../utils/errors"; +import { Transaction } from "../../../../../types/pagopa"; + +export const getPaymentsTransactionDetailsAction = createAsyncAction( + "PAYMENTS_TRANSACTION_DETAILS_REQUEST", + "PAYMENTS_TRANSACTION_DETAILS_SUCCESS", + "PAYMENTS_TRANSACTION_DETAILS_FAILURE", + "PAYMENTS_TRANSACTION_DETAILS_CANCEL" +)<{ transactionId: number }, Transaction, NetworkError, void>(); + +export type PaymentsTransactionActions = ActionType< + typeof getPaymentsTransactionDetailsAction +>; diff --git a/ts/features/payments/transaction/store/reducers/index.ts b/ts/features/payments/transaction/store/reducers/index.ts new file mode 100644 index 00000000000..df3b422f959 --- /dev/null +++ b/ts/features/payments/transaction/store/reducers/index.ts @@ -0,0 +1,47 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../../store/actions/types"; +import { NetworkError } from "../../../../../utils/errors"; + +import { Transaction } from "../../../../../types/pagopa"; +import { getPaymentsTransactionDetailsAction } from "../actions"; + +export type PaymentsTransactionState = { + details: pot.Pot; +}; + +const INITIAL_STATE: PaymentsTransactionState = { + details: pot.noneLoading +}; + +const reducer = ( + state: PaymentsTransactionState = INITIAL_STATE, + action: Action +): PaymentsTransactionState => { + switch (action.type) { + // GET TRANSACTION DETAILS + case getType(getPaymentsTransactionDetailsAction.request): + return { + ...state, + details: pot.toLoading(pot.none) + }; + case getType(getPaymentsTransactionDetailsAction.success): + return { + ...state, + details: pot.some(action.payload) + }; + case getType(getPaymentsTransactionDetailsAction.failure): + return { + ...state, + details: pot.toError(state.details, action.payload) + }; + case getType(getPaymentsTransactionDetailsAction.cancel): + return { + ...state, + details: pot.none + }; + } + return state; +}; + +export default reducer; diff --git a/ts/features/payments/transaction/store/selectors/index.ts b/ts/features/payments/transaction/store/selectors/index.ts new file mode 100644 index 00000000000..a1876232910 --- /dev/null +++ b/ts/features/payments/transaction/store/selectors/index.ts @@ -0,0 +1,7 @@ +import { GlobalState } from "../../../../../store/reducers/types"; + +const walletTransactionSelector = (state: GlobalState) => + state.features.payments.transaction; + +export const walletTransactionDetailsPotSelector = (state: GlobalState) => + walletTransactionSelector(state).details; diff --git a/ts/features/payments/wallet/components/PaymentWalletCard.tsx b/ts/features/payments/wallet/components/PaymentWalletCard.tsx new file mode 100644 index 00000000000..0e827d01fff --- /dev/null +++ b/ts/features/payments/wallet/components/PaymentWalletCard.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Pressable } from "react-native"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { withWalletCardBaseComponent } from "../../../newWallet/components/WalletCardBaseComponent"; +import { + PaymentCard, + PaymentCardProps +} from "../../common/components/PaymentCard"; +import { PaymentsMethodDetailsRoutes } from "../../details/navigation/routes"; + +export type PaymentWalletCardProps = PaymentCardProps & { + walletId: string; +}; + +const WrappedPaymentCard = (props: PaymentWalletCardProps) => { + const navigation = useIONavigation(); + + const { walletId, ...cardProps } = props; + + const handleOnPress = () => { + navigation.navigate( + PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_NAVIGATOR, + { + screen: PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_SCREEN, + params: { + walletId + } + } + ); + }; + + return ( + + + + ); +}; + +/** + * Wrapper component which adds wallet capabilites to the PaymentCard component + */ +export const PaymentWalletCard = + withWalletCardBaseComponent(WrappedPaymentCard); diff --git a/ts/features/payments/wallet/saga/__tests__/handleGetPaymentsWalletUserMethods.test.ts b/ts/features/payments/wallet/saga/__tests__/handleGetPaymentsWalletUserMethods.test.ts new file mode 100644 index 00000000000..fa0950c8904 --- /dev/null +++ b/ts/features/payments/wallet/saga/__tests__/handleGetPaymentsWalletUserMethods.test.ts @@ -0,0 +1,128 @@ +import * as E from "fp-ts/lib/Either"; +import { testSaga } from "redux-saga-test-plan"; +import { getType } from "typesafe-actions"; +import { WalletStatusEnum } from "../../../../../../definitions/pagopa/walletv3/WalletStatus"; +import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; +import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; +import { getPaymentsWalletUserMethods } from "../../store/actions"; +import { handleGetPaymentsWalletUserMethods } from "../handleGetPaymentsWalletUserMethods"; +import { BrandEnum } from "../../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; +import { WalletCard } from "../../../../newWallet/types"; +import { walletAddCards } from "../../../../newWallet/store/actions/cards"; +import { getGenericError } from "../../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../../utils/reporters"; +import { getDateFromExpiryDate } from "../../../../../utils/dates"; + +describe("handleGetPaymentsWalletUserMethods", () => { + it(`should put ${getType(getPaymentsWalletUserMethods.success)} and ${getType( + walletAddCards + )} when response is success`, () => { + const T_WALLETID = "1234"; + const T_HPAN = "0001"; + const T_EXPIRE_DATE = new Date(2027, 10, 1).toDateString(); + const mockGetWalletsByIdUser = jest.fn(); + const getWalletsByIdUserResponse: Wallets = { + wallets: [ + { + walletId: T_WALLETID, + creationDate: new Date(), + paymentMethodId: "paymentMethodId", + paymentMethodAsset: "paymentMethodAsset", + applications: [], + status: WalletStatusEnum.CREATED, + updateDate: new Date(), + details: { + type: "CREDITCARD", + lastFourDigits: T_HPAN, + expiryDate: T_EXPIRE_DATE, + brand: BrandEnum.VISA + } + } + ] + }; + const cards: ReadonlyArray = [ + { + key: `method_${T_WALLETID}`, + type: "payment", + category: "payment", + walletId: T_WALLETID, + hpan: T_HPAN, + brand: BrandEnum.VISA, + expireDate: getDateFromExpiryDate(T_EXPIRE_DATE), + abiCode: undefined, + holderEmail: undefined, + holderPhone: undefined + } + ]; + + testSaga( + handleGetPaymentsWalletUserMethods, + mockGetWalletsByIdUser, + getPaymentsWalletUserMethods.request() + ) + .next() + .call( + withRefreshApiCall, + mockGetWalletsByIdUser(), + getPaymentsWalletUserMethods.request() + ) + .next(E.right({ status: 200, value: getWalletsByIdUserResponse })) + .put(walletAddCards(cards)) + .next() + .put(getPaymentsWalletUserMethods.success(getWalletsByIdUserResponse)) + .next() + .isDone(); + }); + + it(`should put ${getType( + getPaymentsWalletUserMethods.failure + )} when response is not success`, () => { + const mockGetWalletsByIdUser = jest.fn(); + + testSaga( + handleGetPaymentsWalletUserMethods, + mockGetWalletsByIdUser, + getPaymentsWalletUserMethods.request() + ) + .next() + .call( + withRefreshApiCall, + mockGetWalletsByIdUser(), + getPaymentsWalletUserMethods.request() + ) + .next(E.right({ status: 400, value: undefined })) + .put( + getPaymentsWalletUserMethods.failure( + getGenericError(new Error(`Error: 400`)) + ) + ) + .next() + .isDone(); + }); + + it(`should put ${getType( + getPaymentsWalletUserMethods.failure + )} when getWalletsByIdUser encoders returns an error`, () => { + const mockGetWalletsByIdUser = jest.fn(); + + testSaga( + handleGetPaymentsWalletUserMethods, + mockGetWalletsByIdUser, + getPaymentsWalletUserMethods.request() + ) + .next() + .call( + withRefreshApiCall, + mockGetWalletsByIdUser(), + getPaymentsWalletUserMethods.request() + ) + .next(E.left([])) + .put( + getPaymentsWalletUserMethods.failure({ + ...getGenericError(new Error(readablePrivacyReport([]))) + }) + ) + .next() + .isDone(); + }); +}); diff --git a/ts/features/payments/wallet/saga/handleGetPaymentsWalletUserMethods.ts b/ts/features/payments/wallet/saga/handleGetPaymentsWalletUserMethods.ts new file mode 100644 index 00000000000..01490abcc5a --- /dev/null +++ b/ts/features/payments/wallet/saga/handleGetPaymentsWalletUserMethods.ts @@ -0,0 +1,58 @@ +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 { SagaCallReturnType } from "../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../utils/reporters"; +import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { walletAddCards } from "../../../newWallet/store/actions/cards"; +import { WalletClient } from "../../common/api/client"; +import { mapWalletsToCards } from "../../common/utils/wallet"; +import { getPaymentsWalletUserMethods } from "../store/actions"; + +export function* handleGetPaymentsWalletUserMethods( + getWalletsByIdUser: WalletClient["getWalletsByIdUser"], + action: ActionType<(typeof getPaymentsWalletUserMethods)["request"]> +) { + const getWalletsByIdUserRequest = getWalletsByIdUser({}); + + try { + const getWalletsByIdUserResult = (yield* call( + withRefreshApiCall, + getWalletsByIdUserRequest, + action + )) as SagaCallReturnType; + + yield* pipe( + getWalletsByIdUserResult, + E.fold( + function* (error) { + yield* put( + getPaymentsWalletUserMethods.failure( + getGenericError(new Error(readablePrivacyReport(error))) + ) + ); + }, + function* (res) { + if (res.status === 200) { + yield* put( + walletAddCards(mapWalletsToCards(res.value?.wallets || [])) + ); + yield* put(getPaymentsWalletUserMethods.success(res.value)); + } else if (res.status === 404) { + yield* put(getPaymentsWalletUserMethods.success({ wallets: [] })); + } else { + yield* put( + getPaymentsWalletUserMethods.failure({ + ...getGenericError(new Error(`Error: ${res.status}`)) + }) + ); + } + } + ) + ); + } catch (e) { + yield* put(getPaymentsWalletUserMethods.failure({ ...getNetworkError(e) })); + } +} diff --git a/ts/features/payments/wallet/saga/index.ts b/ts/features/payments/wallet/saga/index.ts new file mode 100644 index 00000000000..df0b90a7190 --- /dev/null +++ b/ts/features/payments/wallet/saga/index.ts @@ -0,0 +1,19 @@ +import { SagaIterator } from "redux-saga"; +import { takeLatest } from "typed-redux-saga/macro"; +import { WalletClient } from "../../common/api/client"; +import { getPaymentsWalletUserMethods } from "../store/actions"; +import { handleGetPaymentsWalletUserMethods } from "./handleGetPaymentsWalletUserMethods"; + +/** + * Handle Wallet onboarding requests + * @param bearerToken + */ +export function* watchPaymentsWalletSaga( + walletClient: WalletClient +): SagaIterator { + yield* takeLatest( + getPaymentsWalletUserMethods.request, + handleGetPaymentsWalletUserMethods, + walletClient.getWalletsByIdUser + ); +} diff --git a/ts/features/payments/wallet/store/actions/index.ts b/ts/features/payments/wallet/store/actions/index.ts new file mode 100644 index 00000000000..39f2c4e519f --- /dev/null +++ b/ts/features/payments/wallet/store/actions/index.ts @@ -0,0 +1,13 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; +import { NetworkError } from "../../../../../utils/errors"; + +export const getPaymentsWalletUserMethods = createAsyncAction( + "PAYMENTS_WALLET_GET_USER_METHODS_REQUEST", + "PAYMENTS_WALLET_GET_USER_METHODS_SUCCESS", + "PAYMENTS_WALLET_GET_USER_METHODS_FAILURE" +)(); + +export type PaymentsWalletActions = ActionType< + typeof getPaymentsWalletUserMethods +>; diff --git a/ts/features/payments/wallet/store/reducers/index.ts b/ts/features/payments/wallet/store/reducers/index.ts new file mode 100644 index 00000000000..0271e196e58 --- /dev/null +++ b/ts/features/payments/wallet/store/reducers/index.ts @@ -0,0 +1,40 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../../store/actions/types"; +import { NetworkError } from "../../../../../utils/errors"; +import { getPaymentsWalletUserMethods } from "../actions"; +import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; + +export type PaymentsWalletState = { + userMethods: pot.Pot; +}; + +const INITIAL_STATE: PaymentsWalletState = { + userMethods: pot.none +}; + +const paymentsWalletReducer = ( + state: PaymentsWalletState = INITIAL_STATE, + action: Action +): PaymentsWalletState => { + switch (action.type) { + case getType(getPaymentsWalletUserMethods.request): + return { + ...state, + userMethods: pot.toLoading(pot.none) + }; + case getType(getPaymentsWalletUserMethods.success): + return { + ...state, + userMethods: pot.some(action.payload) + }; + case getType(getPaymentsWalletUserMethods.failure): + return { + ...state, + userMethods: pot.toError(state.userMethods, action.payload) + }; + } + return state; +}; + +export default paymentsWalletReducer; diff --git a/ts/features/pn/analytics/index.ts b/ts/features/pn/analytics/index.ts index b3a1c4ccaa6..e8fbfbfe2dc 100644 --- a/ts/features/pn/analytics/index.ts +++ b/ts/features/pn/analytics/index.ts @@ -4,12 +4,12 @@ import * as O from "fp-ts/lib/Option"; import { mixpanelTrack } from "../../../mixpanel"; import { PNMessage } from "../../pn/store/types/types"; import { NotificationStatusHistoryElement } from "../../../../definitions/pn/NotificationStatusHistoryElement"; -import { UIAttachment } from "../../messages/types"; import { booleanToYesNo, buildEventProperties, numberToYesNoOnThreshold } from "../../../utils/analytics"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; export interface TrackPNPaymentStatus { paymentCount: number; @@ -180,7 +180,7 @@ export function trackPNNotificationLoadSuccess(pnMessage: PNMessage) { buildEventProperties("TECH", undefined, { NOTIFICATION_LAST_STATUS: status, HAS_ATTACHMENTS: pipe( - pnMessage.attachments as Array, + pnMessage.attachments as Array, O.fromNullable, O.map(A.isNonEmpty), O.getOrElse(() => false) diff --git a/ts/features/pn/components/F24ListBottomSheetLink.tsx b/ts/features/pn/components/F24ListBottomSheetLink.tsx new file mode 100644 index 00000000000..408fb84a712 --- /dev/null +++ b/ts/features/pn/components/F24ListBottomSheetLink.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import { StyleSheet, View } from "react-native"; +import { ButtonLink } from "@pagopa/io-app-design-system"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet"; +import I18n from "../../../i18n"; +import { MessageDetailsAttachmentItem } from "../../messages/components/MessageDetail/MessageDetailsAttachmentItem"; +import { UIMessageId } from "../../messages/types"; +import { trackPNShowF24 } from "../analytics"; +import { useIODispatch } from "../../../store/hooks"; +import { cancelPreviousAttachmentDownload } from "../../messages/store/actions"; + +const styles = StyleSheet.create({ + buttonLinkContainer: { + justifyContent: "center", + alignSelf: "center" + } +}); + +type F24ListBottomSheetLinkProps = { + f24List: ReadonlyArray; + messageId: UIMessageId; +}; + +export const F24ListBottomSheetLink = ({ + f24List, + messageId +}: F24ListBottomSheetLinkProps) => { + // The empty footer is needed in order for the internal scroll view to properly compute + // its bottom space when the bottom sheet opens filling the entire view. Without it, the + // scroll bottom stops at the device bottom border, not respecting any safe area margins + const dispatch = useIODispatch(); + const { present, bottomSheet, dismiss } = useIOBottomSheetAutoresizableModal( + { + component: ( + <> + {f24List.map((f24Attachment, index) => ( + { + dismiss(); + }} + /> + ))} + + ), + title: I18n.t("features.pn.details.f24Section.bottomSheet.title"), + footer: , + onDismiss: () => dispatch(cancelPreviousAttachmentDownload()) + }, + 100 + ); + return ( + + { + trackPNShowF24(); + present(); + }} + label={I18n.t("features.pn.details.f24Section.showAll")} + accessibilityLabel={I18n.t("features.pn.details.f24Section.showAll")} + /> + {bottomSheet} + + ); +}; diff --git a/ts/features/pn/components/F24Section.tsx b/ts/features/pn/components/F24Section.tsx new file mode 100644 index 00000000000..27327d7accf --- /dev/null +++ b/ts/features/pn/components/F24Section.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Body, ListItemHeader, VSpacer } from "@pagopa/io-app-design-system"; +import I18n from "../../../i18n"; +import { UIMessageId } from "../../messages/types"; +import { useIOSelector } from "../../../store/hooks"; +import { thirdPartyMessageAttachments } from "../../messages/store/reducers/thirdPartyById"; +import { ATTACHMENT_CATEGORY } from "../../messages/types/attachmentCategory"; +import { MessageDetailsAttachmentItem } from "../../messages/components/MessageDetail/MessageDetailsAttachmentItem"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { F24ListBottomSheetLink } from "./F24ListBottomSheetLink"; + +type F24SectionProps = { + isCancelled?: boolean; + messageId: UIMessageId; + serviceId: ServiceId; +}; + +export const F24Section = ({ + isCancelled = false, + messageId, + serviceId +}: F24SectionProps) => { + const attachments = useIOSelector(state => + thirdPartyMessageAttachments(state, messageId) + ); + const f24s = attachments.filter( + attachment => attachment.category === ATTACHMENT_CATEGORY.F24 + ); + const f24Count = f24s.length; + if (isCancelled || f24Count === 0) { + return null; + } + + return ( + <> + + + {I18n.t("features.pn.details.f24Section.description")} + + + {f24Count === 1 && ( + + )} + {f24Count > 1 && ( + + )} + + + ); +}; diff --git a/ts/features/pn/components/LegacyMessageAttachments.tsx b/ts/features/pn/components/LegacyMessageAttachments.tsx new file mode 100644 index 00000000000..ef401eccc0b --- /dev/null +++ b/ts/features/pn/components/LegacyMessageAttachments.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { View } from "react-native"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { VSpacer, WithTestID } from "@pagopa/io-app-design-system"; +import { ContentTypeValues } from "../../messages/types/contentType"; +import { + LegacyModuleAttachment, + LegacyModuleAttachmentProps +} from "../../messages/components/MessageDetail/LegacyModuleAttachment"; +import { useLegacyAttachmentDownload } from "../../messages/hooks/useLegacyAttachmentDownload"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { UIMessageId } from "../../messages/types"; +import { + attachmentContentType, + attachmentDisplayName +} from "../../messages/store/reducers/transformers"; + +type PartialProps = { + downloadAttachmentBeforePreview?: boolean; + openPreview: (attachment: ThirdPartyAttachment) => void; +}; + +type LegacyMessageAttachmentProps = { + attachment: ThirdPartyAttachment; + messageId: UIMessageId; +} & PartialProps; + +type LegacyMessageAttachmentsProps = WithTestID< + { + attachments: ReadonlyArray; + messageId: UIMessageId; + } & PartialProps +>; + +const getFormatByContentType = ( + contentType: string +): LegacyModuleAttachmentProps["format"] => { + switch (contentType) { + case ContentTypeValues.applicationPdf: + return "pdf"; + default: + return "doc"; + } +}; + +const LegacyAttachmentItem = ({ + attachment, + openPreview, + downloadAttachmentBeforePreview, + messageId +}: LegacyMessageAttachmentProps) => { + const { downloadPot, onAttachmentSelect } = useLegacyAttachmentDownload( + attachment, + messageId, + downloadAttachmentBeforePreview, + openPreview + ); + + const name = attachmentDisplayName(attachment); + const mimeType = attachmentContentType(attachment); + return ( + + ); +}; + +export const LegacyMessageAttachments = ({ + attachments = [], + testID, + ...rest +}: LegacyMessageAttachmentsProps) => ( + + {attachments.map((attachment, index) => ( + + + {index < attachments.length - 1 && } + + ))} + +); diff --git a/ts/features/pn/components/LegacyMessageDetails.tsx b/ts/features/pn/components/LegacyMessageDetails.tsx index 78d59087e5e..063aea1d982 100644 --- a/ts/features/pn/components/LegacyMessageDetails.tsx +++ b/ts/features/pn/components/LegacyMessageDetails.tsx @@ -15,10 +15,9 @@ import { H5 } from "../../../components/core/typography/H5"; import I18n from "../../../i18n"; import { useIOSelector } from "../../../store/hooks"; import { pnFrontendUrlSelector } from "../../../store/reducers/backendStatus"; -import { UIAttachment, UIMessageId } from "../../messages/types"; +import { UIMessageId } from "../../messages/types"; import { clipboardSetStringWithFeedback } from "../../../utils/clipboard"; -import { LegacyMessageAttachments } from "../../messages/components/LegacyMessageAttachments"; -import NavigationService from "../../../navigation/NavigationService"; +import { LegacyMessageAttachments } from "../../messages/components/MessageDetail/LegacyMessageAttachments"; import PN_ROUTES from "../navigation/routes"; import { PNMessage } from "../store/types/types"; import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; @@ -33,14 +32,17 @@ import { import { LevelEnum } from "../../../../definitions/content/SectionStatus"; import { ATTACHMENT_CATEGORY } from "../../messages/types/attachmentCategory"; import { maxVisiblePaymentCountGenerator } from "../utils"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { MESSAGES_ROUTES } from "../../messages/navigation/routes"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { LegacyMessageDetailsContent } from "./LegacyMessageDetailsContent"; import { MessageDetailsHeader } from "./MessageDetailsHeader"; import { MessageDetailsSection } from "./MessageDetailsSection"; import { MessageTimeline } from "./MessageTimeline"; import { MessageTimelineCTA } from "./MessageTimelineCTA"; -import { MessageF24 } from "./MessageF24"; -import { MessagePayments } from "./MessagePayments"; -import { MessageFooter } from "./MessageFooter"; +import { LegacyMessageF24 } from "./LegacyMessageF24"; +import { LegacyMessageFooter } from "./LegacyMessageFooter"; +import { LegacyMessagePayments } from "./LegacyMessagePayments"; import { MessagePaymentBottomSheet } from "./MessagePaymentBottomSheet"; type Props = Readonly<{ @@ -59,11 +61,12 @@ export const LegacyMessageDetails = ({ const viewRef = createRef(); const presentPaymentsBottomSheetRef = useRef<() => void>(); const frontendUrl = useIOSelector(pnFrontendUrlSelector); + const navigation = useIONavigation(); const partitionedAttachments = pipe( message.attachments, O.fromNullable, - O.getOrElse>(() => []), + O.getOrElse>(() => []), RA.partition(attachment => attachment.category === ATTACHMENT_CATEGORY.F24) ); @@ -76,15 +79,20 @@ export const LegacyMessageDetails = ({ : undefined; const openAttachment = useCallback( - (attachment: UIAttachment) => { + (attachment: ThirdPartyAttachment) => { trackPNAttachmentOpening(attachment.category); - NavigationService.navigate(PN_ROUTES.MESSAGE_ATTACHMENT, { - messageId, - attachmentId: attachment.id, - category: attachment.category + navigation.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { + screen: PN_ROUTES.MAIN, + params: { + screen: PN_ROUTES.MESSAGE_ATTACHMENT, + params: { + attachmentId: attachment.id, + messageId + } + } }); }, - [messageId] + [messageId, navigation] ); const maxVisiblePaymentCount = maxVisiblePaymentCountGenerator(); @@ -100,11 +108,11 @@ export const LegacyMessageDetails = ({ ref={scrollViewRef} > {service && } - + {isCancelled && ( <> - + )} - - + ) : null} @@ -181,7 +194,7 @@ export const LegacyMessageDetails = ({ /> )} - ; - openPreview: (attachment: UIAttachment) => void; + attachments: ReadonlyArray; + messageId: UIMessageId; + openPreview: (attachment: ThirdPartyAttachment) => void; }; -const MessageF24Content = ({ attachments, openPreview }: Props) => { - const { present, bottomSheet } = useF24BottomSheet(attachments, openPreview); +const MessageF24Content = ({ attachments, messageId, openPreview }: Props) => { + const { present, bottomSheet } = useF24BottomSheet( + attachments, + messageId, + openPreview + ); if (attachments.length === 1) { return ( - ); @@ -51,16 +58,16 @@ const MessageF24Content = ({ attachments, openPreview }: Props) => { ); }; -export const MessageF24 = (props: Props) => ( +export const LegacyMessageF24 = (props: Props) => ( - + {I18n.t("features.pn.details.f24Section.description")} - + ); diff --git a/ts/features/pn/components/LegacyMessageFooter.tsx b/ts/features/pn/components/LegacyMessageFooter.tsx new file mode 100644 index 00000000000..87dae5d005a --- /dev/null +++ b/ts/features/pn/components/LegacyMessageFooter.tsx @@ -0,0 +1,101 @@ +import React, { MutableRefObject, useCallback } from "react"; +import { StyleSheet, View } from "react-native"; +import { + ButtonSolid, + IOStyles, + useIOToast +} from "@pagopa/io-app-design-system"; +import I18n from "i18n-js"; +import { useDispatch } from "react-redux"; +import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; +import { useIOSelector } from "../../../store/hooks"; +import { UIMessageId } from "../../messages/types"; +import { canNavigateToPaymentFromMessageSelector } from "../../messages/store/reducers/payments"; +import variables from "../../../theme/variables"; +import { getRptIdStringFromPayment } from "../utils/rptId"; +import { trackPNShowAllPayments } from "../analytics"; +import { initializeAndNavigateToWalletForPayment } from "../../messages/utils"; +import { paymentsButtonStateSelector } from "../store/reducers/payments"; + +const styles = StyleSheet.create({ + container: { + overflow: "hidden", + marginTop: -variables.footerShadowOffsetHeight, + paddingTop: variables.footerShadowOffsetHeight + } +}); + +type LegacyMessageFooterProps = { + messageId: UIMessageId; + payments: ReadonlyArray | undefined; + maxVisiblePaymentCount: number; + isCancelled: boolean; + presentPaymentsBottomSheetRef: MutableRefObject<(() => void) | undefined>; +}; + +export const LegacyMessageFooter = ({ + messageId, + payments, + maxVisiblePaymentCount, + isCancelled, + presentPaymentsBottomSheetRef +}: LegacyMessageFooterProps) => { + const buttonState = useIOSelector(state => + paymentsButtonStateSelector( + state, + messageId, + payments, + maxVisiblePaymentCount + ) + ); + const dispatch = useDispatch(); + const toast = useIOToast(); + const canNavigateToPayment = useIOSelector(state => + canNavigateToPaymentFromMessageSelector(state) + ); + const onFooterPressCallback = useCallback(() => { + if (payments?.length === 1) { + const firstPayment = payments[0]; + const paymentId = getRptIdStringFromPayment(firstPayment); + initializeAndNavigateToWalletForPayment( + messageId, + paymentId, + false, + undefined, + canNavigateToPayment, + dispatch, + true, + () => toast.error(I18n.t("genericError")) + ); + } else { + trackPNShowAllPayments(); + presentPaymentsBottomSheetRef.current?.(); + } + }, [ + canNavigateToPayment, + dispatch, + messageId, + payments, + presentPaymentsBottomSheetRef, + toast + ]); + if (isCancelled || buttonState === "hidden") { + return null; + } + const isLoading = buttonState === "visibleLoading"; + return ( + + + + + + ); +}; diff --git a/ts/features/pn/components/LegacyMessagePayments.tsx b/ts/features/pn/components/LegacyMessagePayments.tsx new file mode 100644 index 00000000000..8d4d0283d78 --- /dev/null +++ b/ts/features/pn/components/LegacyMessagePayments.tsx @@ -0,0 +1,224 @@ +import { pipe } from "fp-ts/lib/function"; +import * as RA from "fp-ts/lib/ReadonlyArray"; +import * as O from "fp-ts/lib/Option"; +import React, { MutableRefObject } from "react"; +import { StyleSheet, View } from "react-native"; +import I18n from "i18n-js"; +import { + ButtonLink, + ModulePaymentNotice, + VSpacer +} from "@pagopa/io-app-design-system"; +import { CommonActions, useNavigation } from "@react-navigation/native"; +import Placeholder from "rn-placeholder"; +import { getBadgeTextByPaymentNoticeStatus } from "../../messages/utils/strings"; +import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; +import { InfoBox } from "../../../components/box/InfoBox"; +import { H5 } from "../../../components/core/typography/H5"; +import { UIMessageId } from "../../messages/types"; +import { useIOSelector } from "../../../store/hooks"; +import { paymentsButtonStateSelector } from "../store/reducers/payments"; +import { trackPNShowAllPayments } from "../analytics"; +import PN_ROUTES from "../navigation/routes"; +import { MESSAGES_ROUTES } from "../../messages/navigation/routes"; +import { MessagePaymentItem } from "../../messages/components/MessageDetail/MessagePaymentItem"; +import { getRptIdStringFromPayment } from "../utils/rptId"; +import { MessageDetailsSection } from "./MessageDetailsSection"; + +const styles = StyleSheet.create({ + morePaymentsSkeletonContainer: { + flex: 1, + alignItems: "center" + }, + morePaymentsLinkContainer: { + alignSelf: "center" + } +}); + +type LegacyMessagePaymentsProps = { + messageId: UIMessageId; + isCancelled: boolean; + payments: ReadonlyArray | undefined; + completedPaymentNoticeCodes: ReadonlyArray | undefined; + maxVisiblePaymentCount: number; + presentPaymentsBottomSheetRef: MutableRefObject<(() => void) | undefined>; +}; + +const readonlyArrayHasNoData = (maybeArray: ReadonlyArray | undefined) => + !maybeArray || RA.isEmpty(maybeArray); + +/* + * Skip the payment section when the notification is not cancelled but there are no payments to show + * or + * Skip the payment section when the notification is cancelled and there are no payments nor completed payments to show + */ +const paymentSectionShouldRenderNothing = ( + isCancelled: boolean, + payments: ReadonlyArray | undefined, + completedPaymentNoticeCodes: ReadonlyArray | undefined +) => + (!isCancelled && readonlyArrayHasNoData(payments)) || + (isCancelled && + readonlyArrayHasNoData(payments) && + readonlyArrayHasNoData(completedPaymentNoticeCodes)); + +const generateNavigationToPaidPaymentScreenAction = ( + noticeCode: string, + maybePayments: ReadonlyArray | undefined +) => + pipe( + maybePayments, + O.fromNullable, + O.chain(payments => + pipe( + payments, + RA.findFirst(payment => noticeCode === payment.noticeCode) + ) + ), + O.fold( + () => undefined, + payment => payment.creditorTaxId + ), + maybeCreditorTaxId => + CommonActions.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { + screen: PN_ROUTES.MAIN, + params: { + screen: PN_ROUTES.CANCELLED_MESSAGE_PAID_PAYMENT, + params: { + noticeCode, + creditorTaxId: maybeCreditorTaxId + } + } + }) + ); + +export const LegacyMessagePayments = ({ + messageId, + isCancelled, + payments, + completedPaymentNoticeCodes, + maxVisiblePaymentCount, + presentPaymentsBottomSheetRef +}: LegacyMessagePaymentsProps) => { + const navigation = useNavigation(); + const morePaymentsLinkState = useIOSelector(state => + paymentsButtonStateSelector( + state, + messageId, + payments, + maxVisiblePaymentCount + ) + ); + if ( + paymentSectionShouldRenderNothing( + isCancelled, + payments, + completedPaymentNoticeCodes + ) + ) { + return null; + } + if (isCancelled) { + return ( + + + +
+ {I18n.t("features.pn.details.cancelledMessage.unpaidPayments")} +
+
+ {completedPaymentNoticeCodes && + completedPaymentNoticeCodes.map( + (completedPaymentNoticeCode, index) => ( + + 0 ? 8 : 24} /> + + navigation.dispatch( + generateNavigationToPaidPaymentScreenAction( + completedPaymentNoticeCode, + payments + ) + ) + } + paymentNoticeStatus={"paid"} + badgeText={getBadgeTextByPaymentNoticeStatus("paid")} + testID={"PnCancelledPaymentModulePaymentNotice"} + /> + + ) + )} +
+ ); + } + + const showMorePaymentsLink = + payments && payments.length > maxVisiblePaymentCount; + const morePaymentsLabel = payments + ? `${I18n.t("features.pn.details.paymentSection.morePayments")} (${ + payments.length + })` + : ""; + return ( + + {payments && ( + <> + {payments.slice(0, maxVisiblePaymentCount).map((payment, index) => { + const rptId = getRptIdStringFromPayment(payment); + return ( + + ); + })} + {showMorePaymentsLink && ( + <> + + {morePaymentsLinkState === "visibleLoading" && ( + + + + )} + {morePaymentsLinkState === "visibleEnabled" && ( + + { + trackPNShowAllPayments(); + presentPaymentsBottomSheetRef.current?.(); + }} + /> + + )} + + )} + + )} + + ); +}; diff --git a/ts/features/pn/components/MessageDetails.tsx b/ts/features/pn/components/MessageDetails.tsx index bb5ce071485..90878cb9e82 100644 --- a/ts/features/pn/components/MessageDetails.tsx +++ b/ts/features/pn/components/MessageDetails.tsx @@ -1,59 +1,120 @@ -import React from "react"; -import { ScrollView, View } from "react-native"; -import { pipe } from "fp-ts/lib/function"; +import { + ContentWrapper, + IOStyles, + Tag, + VSpacer +} from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import * as RA from "fp-ts/lib/ReadonlyArray"; import * as SEP from "fp-ts/lib/Separated"; -import { HSpacer, IOStyles, Tag, VSpacer } from "@pagopa/io-app-design-system"; -import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; -import { UIAttachment, UIMessageId } from "../../messages/types"; -import { PNMessage } from "../store/types/types"; +import { pipe } from "fp-ts/lib/function"; +import React, { useRef } from "react"; +import { ScrollView } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; -import { MessageDetailHeader } from "../../messages/components/MessageDetail/MessageDetailHeader"; -import { ATTACHMENT_CATEGORY } from "../../messages/types/attachmentCategory"; import I18n from "../../../i18n"; +import { MessageDetailsAttachments } from "../../messages/components/MessageDetail/MessageDetailsAttachments"; +import { MessageDetailsHeader } from "../../messages/components/MessageDetail/MessageDetailsHeader"; +import { MessageDetailsTagBox } from "../../messages/components/MessageDetail/MessageDetailsTagBox"; +import { UIMessageId } from "../../messages/types"; +import { ATTACHMENT_CATEGORY } from "../../messages/types/attachmentCategory"; +import { PNMessage } from "../store/types/types"; +import { maxVisiblePaymentCountGenerator } from "../utils"; +import { F24Section } from "./F24Section"; import { MessageDetailsContent } from "./MessageDetailsContent"; +import { MessageFooter } from "./MessageFooter"; +import { MessageInfo } from "./MessageInfo"; +import { MessagePayments } from "./MessagePayments"; type MessageDetailsProps = { message: PNMessage; messageId: UIMessageId; - service?: ServicePublic; + serviceId: ServiceId; payments?: ReadonlyArray; }; -export const MessageDetails = ({ message, service }: MessageDetailsProps) => { +export const MessageDetails = ({ + message, + messageId, + payments, + serviceId +}: MessageDetailsProps) => { + const presentPaymentsBottomSheetRef = useRef<() => void>(); + const safeAreaInsets = useSafeAreaInsets(); const partitionedAttachments = pipe( message.attachments, O.fromNullable, - O.getOrElse>(() => []), + O.getOrElse>(() => []), RA.partition(attachment => attachment.category === ATTACHMENT_CATEGORY.F24) ); const attachmentList = SEP.left(partitionedAttachments); + const maxVisiblePaymentCount = maxVisiblePaymentCountGenerator(); + + const isCancelled = message.isCancelled ?? false; + const completedPaymentNoticeCodes = isCancelled + ? message.completedPayments + : undefined; return ( - - - - + + + + + + {attachmentList.length > 0 && ( - <> - - - + + + )} - - - - + +
+ + + + + + + + + + +
); }; diff --git a/ts/features/pn/components/MessageDetailsContent.tsx b/ts/features/pn/components/MessageDetailsContent.tsx index 96cbdbbe3ad..0237cc013b8 100644 --- a/ts/features/pn/components/MessageDetailsContent.tsx +++ b/ts/features/pn/components/MessageDetailsContent.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Body, ContentWrapper, VSpacer } from "@pagopa/io-app-design-system"; +import { Body, VSpacer } from "@pagopa/io-app-design-system"; type MessageDetailsContentProps = { abstract?: string; @@ -13,9 +13,9 @@ export const MessageDetailsContent = ({ } return ( - + <> {abstract} - + ); }; diff --git a/ts/features/pn/components/MessageFooter.tsx b/ts/features/pn/components/MessageFooter.tsx index 6c1c0555c04..02944f80361 100644 --- a/ts/features/pn/components/MessageFooter.tsx +++ b/ts/features/pn/components/MessageFooter.tsx @@ -1,80 +1,38 @@ -import React, { MutableRefObject, useCallback } from "react"; +import { IOColors, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; import { StyleSheet, View } from "react-native"; -import { ButtonSolid, IOStyles } from "@pagopa/io-app-design-system"; -import I18n from "i18n-js"; -import { useDispatch } from "react-redux"; -import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; +import { ContactsListItem } from "../../messages/components/MessageDetail/ContactsListItem"; import { useIOSelector } from "../../../store/hooks"; -import { UIMessageId } from "../../messages/types"; -import { paymentsButtonStateSelector } from "../store/reducers/payments"; -import variables from "../../../theme/variables"; -import { initializeAndNavigateToWalletForPayment } from "../utils"; -import { getRptIdStringFromPayment } from "../utils/rptId"; -import { useIOToast } from "../../../components/Toast"; -import { trackPNShowAllPayments } from "../analytics"; +import { serviceMetadataByIdSelector } from "../../services/store/reducers/servicesById"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; const styles = StyleSheet.create({ container: { - overflow: "hidden", - marginTop: -variables.footerShadowOffsetHeight, - paddingTop: variables.footerShadowOffsetHeight + backgroundColor: IOColors["grey-50"], + paddingBottom: "95%", + marginBottom: "-95%", + paddingHorizontal: IOStyles.horizontalContentPadding.paddingHorizontal, + paddingTop: IOStyles.footer.paddingTop } }); type MessageFooterProps = { - messageId: UIMessageId; - payments: ReadonlyArray | undefined; - maxVisiblePaymentCount: number; - isCancelled: boolean; - presentPaymentsBottomSheetRef: MutableRefObject<(() => void) | undefined>; + serviceId: ServiceId; }; -export const MessageFooter = ({ - messageId, - payments, - maxVisiblePaymentCount, - isCancelled, - presentPaymentsBottomSheetRef -}: MessageFooterProps) => { - const buttonState = useIOSelector(state => - paymentsButtonStateSelector( - state, - messageId, - payments, - maxVisiblePaymentCount - ) +export const MessageFooter = ({ serviceId }: MessageFooterProps) => { + const serviceMetadata = useIOSelector(state => + serviceMetadataByIdSelector(state, serviceId) ); - const dispatch = useDispatch(); - const toast = useIOToast(); - const onFooterPressCallback = useCallback(() => { - if (payments?.length === 1) { - const firstPayment = payments[0]; - const paymentId = getRptIdStringFromPayment(firstPayment); - initializeAndNavigateToWalletForPayment(paymentId, dispatch, () => - toast.error(I18n.t("genericError")) - ); - } else { - trackPNShowAllPayments(); - presentPaymentsBottomSheetRef.current?.(); - } - }, [dispatch, payments, presentPaymentsBottomSheetRef, toast]); - if (isCancelled || buttonState === "hidden") { - return null; - } - const isLoading = buttonState === "visibleLoading"; return ( - - - + )} + ); }; diff --git a/ts/features/pn/components/MessageInfo.tsx b/ts/features/pn/components/MessageInfo.tsx new file mode 100644 index 00000000000..62eb0d1462a --- /dev/null +++ b/ts/features/pn/components/MessageInfo.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { ListItemHeader, ListItemInfoCopy } from "@pagopa/io-app-design-system"; +import I18n from "../../../i18n"; +import { clipboardSetStringWithFeedback } from "../../../utils/clipboard"; + +type MessageInfoProps = { + iun: string; +}; + +export const MessageInfo = ({ iun }: MessageInfoProps) => ( + <> + + clipboardSetStringWithFeedback(iun)} + /> + +); diff --git a/ts/features/pn/components/MessagePaymentBottomSheet.tsx b/ts/features/pn/components/MessagePaymentBottomSheet.tsx index fd81a904d01..9a1a7dc1b7a 100644 --- a/ts/features/pn/components/MessagePaymentBottomSheet.tsx +++ b/ts/features/pn/components/MessagePaymentBottomSheet.tsx @@ -5,8 +5,9 @@ import { useDispatch } from "react-redux"; import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; import { UIMessageId } from "../../messages/types"; import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; -import { cancelQueuedPaymentUpdates } from "../store/actions"; -import { MessagePaymentItem } from "./MessagePaymentItem"; +import { cancelQueuedPaymentUpdates } from "../../messages/store/actions"; +import { MessagePaymentItem } from "../../messages/components/MessageDetail/MessagePaymentItem"; +import { getRptIdStringFromPayment } from "../utils/rptId"; export type MessagePaymentBottomSheetProps = { messageId: UIMessageId; @@ -26,16 +27,21 @@ export const MessagePaymentBottomSheet = ({ const { present, dismiss, bottomSheet } = useIOBottomSheetModal({ component: ( <> - {payments.map((payment, index) => ( - dismiss()} - /> - ))} + {payments.map((payment, index) => { + const rptId = getRptIdStringFromPayment(payment); + return ( + dismiss()} + /> + ); + })} ), title: I18n.t("features.pn.details.paymentSection.bottomSheetTitle"), diff --git a/ts/features/pn/components/MessagePayments.tsx b/ts/features/pn/components/MessagePayments.tsx index 2bccac60d63..d2629b01096 100644 --- a/ts/features/pn/components/MessagePayments.tsx +++ b/ts/features/pn/components/MessagePayments.tsx @@ -6,6 +6,8 @@ import { StyleSheet, View } from "react-native"; import I18n from "i18n-js"; import { ButtonLink, + FeatureInfo, + ListItemHeader, ModulePaymentNotice, VSpacer } from "@pagopa/io-app-design-system"; @@ -13,21 +15,19 @@ import { CommonActions, useNavigation } from "@react-navigation/native"; import Placeholder from "rn-placeholder"; import { getBadgeTextByPaymentNoticeStatus } from "../../messages/utils/strings"; import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; -import { InfoBox } from "../../../components/box/InfoBox"; -import { H5 } from "../../../components/core/typography/H5"; import { UIMessageId } from "../../messages/types"; import { useIOSelector } from "../../../store/hooks"; import { paymentsButtonStateSelector } from "../store/reducers/payments"; import { trackPNShowAllPayments } from "../analytics"; import PN_ROUTES from "../navigation/routes"; import { MESSAGES_ROUTES } from "../../messages/navigation/routes"; -import { MessageDetailsSection } from "./MessageDetailsSection"; -import { MessagePaymentItem } from "./MessagePaymentItem"; +import { MessagePaymentItem } from "../../messages/components/MessageDetail/MessagePaymentItem"; +import { getRptIdStringFromPayment } from "../utils/rptId"; const styles = StyleSheet.create({ morePaymentsSkeletonContainer: { flex: 1, - alignItems: "center" + alignItems: "flex-start" }, morePaymentsLinkContainer: { alignSelf: "center" @@ -119,21 +119,14 @@ export const MessagePayments = ({ } if (isCancelled) { return ( - - - -
- {I18n.t("features.pn.details.cancelledMessage.unpaidPayments")} -
-
+ <> + + {completedPaymentNoticeCodes && completedPaymentNoticeCodes.map( (completedPaymentNoticeCode, index) => ( @@ -157,7 +150,7 @@ export const MessagePayments = ({
) )} - + ); } @@ -169,24 +162,31 @@ export const MessagePayments = ({ })` : ""; return ( - + <> + {payments && ( <> - {payments.slice(0, maxVisiblePaymentCount).map((payment, index) => ( - - ))} + {payments.slice(0, maxVisiblePaymentCount).map((payment, index) => { + const rptId = getRptIdStringFromPayment(payment); + return ( + + ); + })} {showMorePaymentsLink && ( <> - + {morePaymentsLinkState === "visibleLoading" && ( )} + )} )} - + ); }; diff --git a/ts/features/pn/components/ServiceCTA.tsx b/ts/features/pn/components/ServiceCTA.tsx index e2665f3936f..70101020779 100644 --- a/ts/features/pn/components/ServiceCTA.tsx +++ b/ts/features/pn/components/ServiceCTA.tsx @@ -9,15 +9,15 @@ import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; import { Label } from "../../../components/core/typography/Label"; import I18n from "../../../i18n"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; -import { servicePreferenceSelector } from "../../../store/reducers/entities/services/servicePreference"; -import { isServicePreferenceResponseSuccess } from "../../../types/services/ServicePreferenceResponse"; +import { servicePreferenceSelector } from "../../services/store/reducers/servicePreference"; +import { isServicePreferenceResponseSuccess } from "../../services/types/ServicePreferenceResponse"; import { AppDispatch } from "../../../App"; import { pnActivationUpsert } from "../store/actions"; import { pnActivationSelector } from "../store/reducers/activation"; import { showToast } from "../../../utils/showToast"; import { Link } from "../../../components/core/typography/Link"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; -import { loadServicePreference } from "../../../store/actions/services/servicePreference"; +import { loadServicePreference } from "../../services/store/actions"; import { trackPNServiceActivated, trackPNServiceDeactivated, diff --git a/ts/features/pn/components/__test__/F24ListBottomSheetLink.test.tsx b/ts/features/pn/components/__test__/F24ListBottomSheetLink.test.tsx new file mode 100644 index 00000000000..a022d99d125 --- /dev/null +++ b/ts/features/pn/components/__test__/F24ListBottomSheetLink.test.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { F24ListBottomSheetLink } from "../F24ListBottomSheetLink"; +import { UIMessageId } from "../../../messages/types"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; + +const numberToThirdPartyAttachment = (index: number) => + ({ + id: `${index}`, + url: `https://domain.url/doc${index}.pdf` + } as ThirdPartyAttachment); + +describe("F24ListBottomSheetLink", () => { + it("should be snapshot for an 0 items F24 list", () => { + const zeroF24List = [] as ReadonlyArray; + const component = renderComponent(zeroF24List); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should be snapshot for an 1 item F24 list", () => { + const oneF24List = [...Array(1).keys()].map(numberToThirdPartyAttachment); + const component = renderComponent(oneF24List); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should be snapshot for a 10 items F24 list", () => { + const oneF24List = [...Array(10).keys()].map(numberToThirdPartyAttachment); + const component = renderComponent(oneF24List); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = (f24List: ReadonlyArray) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/F24Section.test.tsx b/ts/features/pn/components/__test__/F24Section.test.tsx new file mode 100644 index 00000000000..aa9a847c73d --- /dev/null +++ b/ts/features/pn/components/__test__/F24Section.test.tsx @@ -0,0 +1,133 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { F24Section } from "../F24Section"; +import { appReducer } from "../../../../store/reducers"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { UIMessageId } from "../../../messages/types"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import * as thirdPartyById from "../../../messages/store/reducers/thirdPartyById"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { ATTACHMENT_CATEGORY } from "../../../messages/types/attachmentCategory"; + +const generateOneAttachmentArray = () => [ + { + id: "1", + url: "https://no.url/doc.pdf" + } as ThirdPartyAttachment +]; +const generateThreeAttachmentArray = () => [ + { + id: "1", + url: "https://no.url/doc.pdf" + } as ThirdPartyAttachment, + { + id: "2", + url: "https://no.url/docF24.pdf", + category: ATTACHMENT_CATEGORY.F24 + } as ThirdPartyAttachment, + { + id: "3", + url: "https://no.url/cod.pdf" + } as ThirdPartyAttachment +]; +const generateFourAttachmentArray = () => [ + { + id: "1", + url: "https://no.url/doc.pdf" + } as ThirdPartyAttachment, + { + id: "2", + url: "https://no.url/docF24.pdf", + category: ATTACHMENT_CATEGORY.F24 + } as ThirdPartyAttachment, + { + id: "3", + url: "https://no.url/cod.pdf" + } as ThirdPartyAttachment, + { + id: "4", + url: "https://no.url/f24Doc.pdf", + category: ATTACHMENT_CATEGORY.F24 + } as ThirdPartyAttachment +]; + +describe("F24Section", () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + it("should match snapshot when there are no F24", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => generateOneAttachmentArray()); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when there are no F24 and the message is cancelled", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => generateOneAttachmentArray()); + const component = renderComponent(true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when there is a single F24", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => + generateThreeAttachmentArray() + ); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when there is a single F24 and the message is cancelled", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => + generateThreeAttachmentArray() + ); + const component = renderComponent(true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when there are more than one F24", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => + generateFourAttachmentArray() + ); + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when there are more than one F24 and the message is cancelled", () => { + jest + .spyOn(thirdPartyById, "thirdPartyMessageAttachments") + .mockImplementation((_state, _messageId) => + generateFourAttachmentArray() + ); + const component = renderComponent(true); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = (isCancelled: boolean = false) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/__test__/MessageAttachments.test.tsx b/ts/features/pn/components/__test__/LegacyMessageAttachments.test.tsx similarity index 82% rename from ts/features/messages/components/__test__/MessageAttachments.test.tsx rename to ts/features/pn/components/__test__/LegacyMessageAttachments.test.tsx index fae4747db93..9d85e4d5424 100644 --- a/ts/features/messages/components/__test__/MessageAttachments.test.tsx +++ b/ts/features/pn/components/__test__/LegacyMessageAttachments.test.tsx @@ -6,11 +6,12 @@ import { applicationChangeState } from "../../../../store/actions/application"; import { appReducer } from "../../../../store/reducers"; import { GlobalState } from "../../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; -import { MessageAttachments } from "../MessageAttachments"; -import { Downloads } from "../../store/reducers/downloads"; -import { mockPdfAttachment } from "../../__mocks__/attachment"; -import { downloadAttachment } from "../../store/actions"; +import { LegacyMessageAttachments } from "../LegacyMessageAttachments"; +import { Downloads } from "../../../messages/store/reducers/downloads"; +import { mockPdfAttachment } from "../../../messages/__mocks__/attachment"; +import { downloadAttachment } from "../../../messages/store/actions"; import I18n from "../../../../i18n"; +import { messageId_1 } from "../../../messages/__mocks__/messages"; const mockOpenPreview = jest.fn(); const mockShowToast = jest.fn(); @@ -19,7 +20,7 @@ jest.mock("../../../../utils/showToast", () => ({ showToast: () => mockShowToast() })); -describe("MessageAttachments", () => { +describe("LegacyMessageAttachments", () => { beforeEach(() => { mockShowToast.mockReset(); mockOpenPreview.mockReset(); @@ -35,10 +36,11 @@ describe("MessageAttachments", () => { const { component } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: loadingPot } } @@ -66,10 +68,11 @@ describe("MessageAttachments", () => { const { component } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: notLoadingPot } } @@ -88,10 +91,11 @@ describe("MessageAttachments", () => { const { store } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: jest.fn() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.noneLoading } } @@ -101,6 +105,7 @@ describe("MessageAttachments", () => { store.dispatch( downloadAttachment.failure({ attachment: mockPdfAttachment, + messageId: messageId_1, error: new Error() }) ) @@ -114,10 +119,11 @@ describe("MessageAttachments", () => { const { store } = renderComponent( { attachments: [mockPdfAttachment], + messageId: messageId_1, openPreview: mockOpenPreview() }, { - [mockPdfAttachment.messageId]: { + [messageId_1]: { [mockPdfAttachment.id]: pot.noneLoading } } @@ -127,6 +133,7 @@ describe("MessageAttachments", () => { store.dispatch( downloadAttachment.success({ path: "path", + messageId: messageId_1, attachment: mockPdfAttachment }) ) @@ -138,7 +145,7 @@ describe("MessageAttachments", () => { }); const renderComponent = ( - props: React.ComponentProps, + props: React.ComponentProps, downloads: Downloads = {} ) => { const globalState = appReducer(undefined, applicationChangeState("active")); @@ -155,7 +162,7 @@ const renderComponent = ( return { component: renderScreenWithNavigationStoreContext( - () => , + () => , "DUMMY", {}, store diff --git a/ts/features/pn/components/__test__/LegacyMessageDetails.test.tsx b/ts/features/pn/components/__test__/LegacyMessageDetails.test.tsx index a7ec40d4d30..402fa3aa99e 100644 --- a/ts/features/pn/components/__test__/LegacyMessageDetails.test.tsx +++ b/ts/features/pn/components/__test__/LegacyMessageDetails.test.tsx @@ -8,18 +8,19 @@ import { LegacyMessageDetails } from "../LegacyMessageDetails"; import { GlobalState } from "../../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; import PN_ROUTES from "../../navigation/routes"; -import { UIAttachment, UIMessageId } from "../../../messages/types"; +import { UIMessageId } from "../../../messages/types"; import { PNMessage } from "../../store/types/types"; import { Download } from "../../../messages/store/reducers/downloads"; import { NotificationRecipient } from "../../../../../definitions/pn/NotificationRecipient"; import { ATTACHMENT_CATEGORY } from "../../../messages/types/attachmentCategory"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; const mockedOnAttachmentSelect = jest.fn(); -jest.mock("../../../messages/hooks/useAttachmentDownload", () => ({ - useAttachmentDownload: ( - _attachment: UIAttachment, - _openPreview: (attachment: UIAttachment) => void +jest.mock("../../../messages/hooks/useLegacyAttachmentDownload", () => ({ + useLegacyAttachmentDownload: ( + _attachment: ThirdPartyAttachment, + _openPreview: (attachment: ThirdPartyAttachment) => void ) => ({ onAttachmentSelect: mockedOnAttachmentSelect, downloadPot: { kind: "PotNone" } as pot.Pot @@ -146,22 +147,20 @@ const generatePnMessage = (): PNMessage => ({ ] as Array, attachments: [ { - messageId: generateTestMessageId(), id: "1", - displayName: "A First Attachment", - contentType: "application/pdf", + name: "A First Attachment", + content_type: "application/pdf", category: ATTACHMENT_CATEGORY.DOCUMENT, - resourceUrl: { href: "/resource/attachment1.pdf" } + url: "/resource/attachment1.pdf" }, { - messageId: generateTestMessageId(), id: "2", - displayName: "A Second Attachment", - contentType: "application/pdf", + name: "A Second Attachment", + content_type: "application/pdf", category: ATTACHMENT_CATEGORY.DOCUMENT, - resourceUrl: { href: "/resource/attachment2.pdf" } + url: "/resource/attachment2.pdf" } - ] as Array + ] as Array }); const generateComponentProperties = (pnMessage: PNMessage) => ({ payments: undefined, diff --git a/ts/features/pn/components/__test__/MessageF24.test.tsx b/ts/features/pn/components/__test__/LegacyMessageF24.test.tsx similarity index 83% rename from ts/features/pn/components/__test__/MessageF24.test.tsx rename to ts/features/pn/components/__test__/LegacyMessageF24.test.tsx index 688d10fc5bc..c2e1ee55c0a 100644 --- a/ts/features/pn/components/__test__/MessageF24.test.tsx +++ b/ts/features/pn/components/__test__/LegacyMessageF24.test.tsx @@ -2,8 +2,7 @@ import React from "react"; import { createStore } from "redux"; import { act, fireEvent, within } from "@testing-library/react-native"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { UIAttachment } from "../../../messages/types"; -import { MessageF24 } from "../MessageF24"; +import { LegacyMessageF24 } from "../LegacyMessageF24"; import I18n from "../../../../i18n"; import { mockOtherAttachment, @@ -13,13 +12,15 @@ import { appReducer } from "../../../../store/reducers"; import { applicationChangeState } from "../../../../store/actions/application"; import { Download } from "../../../messages/store/reducers/downloads"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; +import { messageId_1 } from "../../../messages/__mocks__/messages"; const mockOpenPreview = jest.fn(); -jest.mock("../../../messages/hooks/useAttachmentDownload", () => ({ - useAttachmentDownload: ( - _attachment: UIAttachment, - _openPreview: (attachment: UIAttachment) => void +jest.mock("../../../messages/hooks/useLegacyAttachmentDownload", () => ({ + useLegacyAttachmentDownload: ( + _attachment: ThirdPartyAttachment, + _openPreview: (attachment: ThirdPartyAttachment) => void ) => ({ onAttachmentSelect: mockOpenPreview, downloadPot: { kind: "PotNone" } as pot.Pot @@ -110,14 +111,20 @@ describe("MessageF24 component", () => { }); const renderComponent = ( - attachments: ReadonlyArray, - openPreview: (attachment: UIAttachment) => void + attachments: ReadonlyArray, + openPreview: (attachment: ThirdPartyAttachment) => void ) => { const globalState = appReducer(undefined, applicationChangeState("active")); const store = createStore(appReducer, globalState as any); return renderScreenWithNavigationStoreContext( - () => , + () => ( + + ), "DUMMY", {}, store diff --git a/ts/features/pn/components/__test__/LegacyMessageFooter.test.tsx b/ts/features/pn/components/__test__/LegacyMessageFooter.test.tsx new file mode 100644 index 00000000000..c69e9f3d332 --- /dev/null +++ b/ts/features/pn/components/__test__/LegacyMessageFooter.test.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { UIMessageId } from "../../../messages/types"; +import { LegacyMessageFooter } from "../LegacyMessageFooter"; +import * as standardPayments from "../../../messages/store/reducers/payments"; +import * as payments from "../../store/reducers/payments"; + +describe("LegacyMessageFooter", () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + jest + .spyOn(standardPayments, "canNavigateToPaymentFromMessageSelector") + .mockReturnValue(true); + }); + it("should match snapshot for cancelled PN notification", () => { + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockReturnValue("visibleEnabled"); + const component = renderScreen(true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot for hidden button", () => { + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockReturnValue("hidden"); + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot for visibleLoading button", () => { + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockReturnValue("visibleLoading"); + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot for visibleEnabled button", () => { + jest + .spyOn(payments, "paymentsButtonStateSelector") + .mockReturnValue("visibleEnabled"); + const component = renderScreen(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = ( + isCancelled: boolean = false, + messageId: UIMessageId = "01HRAAFS3VJAAKWKV8NM8Z6CPQ" as UIMessageId, + maxVisiblePaymentCount: number = 5 +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + const mockRef = { + current: jest.fn() + }; + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/LegacyMessagePayments.test.tsx b/ts/features/pn/components/__test__/LegacyMessagePayments.test.tsx new file mode 100644 index 00000000000..280625f807f --- /dev/null +++ b/ts/features/pn/components/__test__/LegacyMessagePayments.test.tsx @@ -0,0 +1,247 @@ +import React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { UIMessageId } from "../../../messages/types"; +import { NotificationPaymentInfo } from "../../../../../definitions/pn/NotificationPaymentInfo"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { LegacyMessagePayments } from "../LegacyMessagePayments"; +import { updatePaymentForMessage } from "../../../messages/store/actions"; + +describe("LegacyMessagePayments component", () => { + it("Should match the snapshot for a single loading payment", () => { + const messageId = "m1" as UIMessageId; + const payments = [ + { + creditorTaxId: "c1", + noticeCode: "n1" + } + ] as Array; + const component = renderComponent( + messageId, + false, + payments, + undefined, + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for five loading payments", () => { + const messageId = "m1" as UIMessageId; + const payments = [ + { + creditorTaxId: "c1", + noticeCode: "n1" + }, + { + creditorTaxId: "c1", + noticeCode: "n2" + }, + { + creditorTaxId: "c1", + noticeCode: "n3" + }, + { + creditorTaxId: "c1", + noticeCode: "n4" + }, + { + creditorTaxId: "c1", + noticeCode: "n5" + } + ] as Array; + const component = renderComponent( + messageId, + false, + payments, + undefined, + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for more than five loading payments", () => { + const messageId = "m1" as UIMessageId; + const payments = [ + { + creditorTaxId: "c1", + noticeCode: "n1" + }, + { + creditorTaxId: "c1", + noticeCode: "n2" + }, + { + creditorTaxId: "c1", + noticeCode: "n3" + }, + { + creditorTaxId: "c1", + noticeCode: "n4" + }, + { + creditorTaxId: "c1", + noticeCode: "n5" + }, + { + creditorTaxId: "c1", + noticeCode: "n6" + } + ] as Array; + const component = renderComponent( + messageId, + false, + payments, + undefined, + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for five payable payments", () => { + const messageId = "m1" as UIMessageId; + const payments = [ + { + creditorTaxId: "c1", + noticeCode: "n1" + }, + { + creditorTaxId: "c1", + noticeCode: "n2" + }, + { + creditorTaxId: "c1", + noticeCode: "n3" + }, + { + creditorTaxId: "c1", + noticeCode: "n4" + }, + { + creditorTaxId: "c1", + noticeCode: "n5" + } + ] as Array; + const component = renderComponent( + messageId, + false, + payments, + "payable", + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for five processed payments", () => { + const messageId = "m1" as UIMessageId; + const payments = [ + { + creditorTaxId: "c1", + noticeCode: "n1" + }, + { + creditorTaxId: "c1", + noticeCode: "n2" + }, + { + creditorTaxId: "c1", + noticeCode: "n3" + }, + { + creditorTaxId: "c1", + noticeCode: "n4" + }, + { + creditorTaxId: "c1", + noticeCode: "n5" + } + ] as Array; + const component = renderComponent( + messageId, + false, + payments, + "processed", + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for a cancelled message with no completed payments", () => { + const messageId = "m1" as UIMessageId; + const component = renderComponent( + messageId, + true, + undefined, + undefined, + undefined + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot for a cancelled message with three completed payments", () => { + const messageId = "m1" as UIMessageId; + const component = renderComponent(messageId, true, undefined, undefined, [ + "n1", + "n2", + "n3" + ]); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = ( + messageId: UIMessageId, + isCancelled: boolean, + payments: ReadonlyArray | undefined, + paymentsStatus: "payable" | "processed" | undefined, + completedPaymentNoticeCodes: ReadonlyArray | undefined +) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const modifiedState = + payments?.reduce( + (previousState, payment) => + paymentsStatus === "payable" + ? appReducer( + previousState, + updatePaymentForMessage.success({ + messageId, + paymentId: ``, + paymentData: { + codiceContestoPagamento: `${payment.noticeCode}`, + importoSingoloVersamento: 99, + causaleVersamento: "Causale", + dueDate: new Date(2023, 10, 23, 10, 30) + } as PaymentRequestsGetResponse + }) + ) + : paymentsStatus === "processed" + ? appReducer( + previousState, + updatePaymentForMessage.failure({ + messageId, + paymentId: ``, + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ) + : previousState, + globalState + ) ?? globalState; + const store = createStore(appReducer, modifiedState as any); + + const mockPresentPaymentsBottomSheetRef = { + current: () => undefined + }; + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/MessageDetails.test.tsx b/ts/features/pn/components/__test__/MessageDetails.test.tsx index a87f86291be..89d21deef44 100644 --- a/ts/features/pn/components/__test__/MessageDetails.test.tsx +++ b/ts/features/pn/components/__test__/MessageDetails.test.tsx @@ -10,28 +10,23 @@ import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWr import { PNMessage } from "../../store/types/types"; import { thirdPartyMessage } from "../../__mocks__/message"; import { toPNMessage } from "../../store/types/transformers"; -import { UIMessageId } from "../../../messages/types"; import I18n from "../../../../i18n"; +import { serviceId_1 } from "../../../messages/__mocks__/messages"; +import { UIMessageId } from "../../../messages/types"; const pnMessage = pipe(thirdPartyMessage, toPNMessage, O.toUndefined)!; describe("MessageDetails component", () => { it("should match the snapshot with default props", () => { const { component } = renderComponent( - generateComponentProperties( - thirdPartyMessage.id as UIMessageId, - pnMessage - ) + generateComponentProperties(pnMessage) ); expect(component).toMatchSnapshot(); }); it("should display the legalMessage tag", () => { const { component } = renderComponent( - generateComponentProperties( - thirdPartyMessage.id as UIMessageId, - pnMessage - ) + generateComponentProperties(pnMessage) ); expect( component.queryByText(I18n.t("features.pn.details.badge.legalValue")) @@ -40,17 +35,14 @@ describe("MessageDetails component", () => { it("should display the attachment tag if there are attachments", () => { const { component } = renderComponent( - generateComponentProperties( - thirdPartyMessage.id as UIMessageId, - pnMessage - ) + generateComponentProperties(pnMessage) ); expect(component.queryByTestId("attachment-tag")).not.toBeNull(); }); it("should NOT display the attachment tag if there are no attachments", () => { const { component } = renderComponent( - generateComponentProperties(thirdPartyMessage.id as UIMessageId, { + generateComponentProperties({ ...pnMessage, attachments: [] }) @@ -59,14 +51,11 @@ describe("MessageDetails component", () => { }); }); -const generateComponentProperties = ( - messageId: UIMessageId, - message: PNMessage -) => ({ - messageId, +const generateComponentProperties = (message: PNMessage) => ({ + messageId: "01HRYR6C761DGH3S84HBBXMMKT" as UIMessageId, message, payments: undefined, - service: undefined + serviceId: serviceId_1 }); const renderComponent = ( diff --git a/ts/features/pn/components/__test__/MessageFooter.test.tsx b/ts/features/pn/components/__test__/MessageFooter.test.tsx new file mode 100644 index 00000000000..d923366787e --- /dev/null +++ b/ts/features/pn/components/__test__/MessageFooter.test.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import PN_ROUTES from "../../navigation/routes"; +import { MessageFooter } from "../MessageFooter"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; + +describe("MessageFooter", () => { + it("should match snapshot", () => { + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = () => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + PN_ROUTES.MESSAGE_DETAILS, + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/MessageInfo.test.tsx b/ts/features/pn/components/__test__/MessageInfo.test.tsx new file mode 100644 index 00000000000..48cbc02f0a3 --- /dev/null +++ b/ts/features/pn/components/__test__/MessageInfo.test.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { createStore } from "redux"; +import { appReducer } from "../../../../store/reducers"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { MessageInfo } from "../MessageInfo"; +import PN_ROUTES from "../../navigation/routes"; + +describe("MessageInfo", () => { + it("should match snapshot", () => { + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const newDesignSystemState = appReducer( + globalState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, newDesignSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => , + PN_ROUTES.MESSAGE_DETAILS, + {}, + store + ); +}; diff --git a/ts/features/pn/components/__test__/MessagePayments.test.tsx b/ts/features/pn/components/__test__/MessagePayments.test.tsx index fc4a7bb7f6d..e7800c243a9 100644 --- a/ts/features/pn/components/__test__/MessagePayments.test.tsx +++ b/ts/features/pn/components/__test__/MessagePayments.test.tsx @@ -1,230 +1,483 @@ import React from "react"; import { createStore } from "redux"; -import { applicationChangeState } from "../../../../store/actions/application"; import { appReducer } from "../../../../store/reducers"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import PN_ROUTES from "../../navigation/routes"; +import { MessagePayments } from "../MessagePayments"; import { UIMessageId } from "../../../messages/types"; import { NotificationPaymentInfo } from "../../../../../definitions/pn/NotificationPaymentInfo"; -import { updatePaymentForMessage } from "../../store/actions"; +import { GlobalState } from "../../../../store/reducers/types"; +import { remoteError, remoteReady } from "../../../../common/model/RemoteValue"; import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; -import { MessagePayments } from "../MessagePayments"; -describe("MessagePayments component", () => { - it("Should match the snapshot for a single loading payment", () => { - const messageId = "m1" as UIMessageId; - const payments = [ - { - creditorTaxId: "c1", - noticeCode: "n1" +const globalMessageId = "01HTFFDYS8VQ779EA4M5WB9YWA" as UIMessageId; +const globalMaxVisiblePaymentCount = 5; +const globalDueDate = new Date(2099, 4, 2, 1, 1, 1); +const generatePayablePayment = ( + codiceContestoPagamento: string, + amount: number +) => + ({ + codiceContestoPagamento, + importoSingoloVersamento: amount, + dueDate: globalDueDate, + causaleVersamento: "hendrerit orci id dolor consectetur" + } as PaymentRequestsGetResponse); +const notificationPaymentInfosFromPaymentIds = (paymentIds: Array) => + paymentIds.map( + payment => + ({ + creditorTaxId: payment.substring(0, 11), + noticeCode: payment.substring(11) + } as NotificationPaymentInfo) + ); + +describe("MessagePayments", () => { + it("should match snapshot when cancelled, without payments, without cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const component = renderComponent( + globalMessageId, + true, + [], + [], + initialState + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when cancelled with payments without completed payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ), + [paymentIds[5]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_SCONOSCIUTO + ), + [paymentIds[6]]: remoteError(Detail_v2Enum.GENERIC_ERROR) + } + } + } } - ] as Array; + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, - false, + globalMessageId, + true, + payments, + [], + paymentsState + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when cancelled, without payments, with cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const component = renderComponent( + globalMessageId, + true, + [], + paymentIds, + initialState + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when cancelled, with payments, with cancelled-completed-payments", () => { + const messageId = "01HTFFDYS8VQ779EA4M5WB9YWA" as UIMessageId; + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [messageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ), + [paymentIds[5]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_SCONOSCIUTO + ), + [paymentIds[6]]: remoteError(Detail_v2Enum.GENERIC_ERROR) + } + } + } + } + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); + const component = renderComponent( + globalMessageId, + true, payments, - undefined, - undefined + paymentIds, + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for five loading payments", () => { - const messageId = "m1" as UIMessageId; - const payments = [ - { - creditorTaxId: "c1", - noticeCode: "n1" - }, - { - creditorTaxId: "c1", - noticeCode: "n2" - }, - { - creditorTaxId: "c1", - noticeCode: "n3" - }, - { - creditorTaxId: "c1", - noticeCode: "n4" - }, - { - creditorTaxId: "c1", - noticeCode: "n5" + it("should match snapshot when not cancelled, without payments, without cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const component = renderComponent( + globalMessageId, + false, + [], + [], + initialState + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when not cancelled, without payments, with cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const component = renderComponent( + globalMessageId, + false, + [], + paymentIds, + initialState + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when not cancelled, with one payable payment, with cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = ["01234567890012345678912345610"]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ) + } + } + } } - ] as Array; + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, + globalMessageId, false, payments, - undefined, - undefined + paymentIds, + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for more than five loading payments", () => { - const messageId = "m1" as UIMessageId; - const payments = [ - { - creditorTaxId: "c1", - noticeCode: "n1" - }, - { - creditorTaxId: "c1", - noticeCode: "n2" - }, - { - creditorTaxId: "c1", - noticeCode: "n3" - }, - { - creditorTaxId: "c1", - noticeCode: "n4" - }, - { - creditorTaxId: "c1", - noticeCode: "n5" - }, - { - creditorTaxId: "c1", - noticeCode: "n6" + it("should match snapshot when not cancelled, with one payable payment, without cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = ["01234567890012345678912345610"]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ) + } + } + } } - ] as Array; + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, + globalMessageId, false, payments, - undefined, - undefined + [], + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for five payable payments", () => { - const messageId = "m1" as UIMessageId; - const payments = [ - { - creditorTaxId: "c1", - noticeCode: "n1" - }, - { - creditorTaxId: "c1", - noticeCode: "n2" - }, - { - creditorTaxId: "c1", - noticeCode: "n3" - }, - { - creditorTaxId: "c1", - noticeCode: "n4" - }, - { - creditorTaxId: "c1", - noticeCode: "n5" + it("should match snapshot when not cancelled, with five (max-visible-payments) payable payments, with cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ) + } + } + } } - ] as Array; + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, + globalMessageId, false, payments, - "payable", - undefined + paymentIds, + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for five processed payments", () => { - const messageId = "m1" as UIMessageId; - const payments = [ - { - creditorTaxId: "c1", - noticeCode: "n1" - }, - { - creditorTaxId: "c1", - noticeCode: "n2" - }, - { - creditorTaxId: "c1", - noticeCode: "n3" - }, - { - creditorTaxId: "c1", - noticeCode: "n4" - }, - { - creditorTaxId: "c1", - noticeCode: "n5" + it("should match snapshot when not cancelled, with five (max-visible-payments) payable payments, without cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ) + } + } + } } - ] as Array; + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, + globalMessageId, false, payments, - "processed", - undefined + [], + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for a cancelled message with no completed payments", () => { - const messageId = "m1" as UIMessageId; + it("should match snapshot when not cancelled, with more-than-five (max-visible-payments) payable payments, with cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ), + [paymentIds[5]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_SCONOSCIUTO + ), + [paymentIds[6]]: remoteError(Detail_v2Enum.GENERIC_ERROR) + } + } + } + } + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); const component = renderComponent( - messageId, - true, - undefined, - undefined, - undefined + globalMessageId, + false, + payments, + paymentIds, + paymentsState ); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match the snapshot for a cancelled message with three completed payments", () => { - const messageId = "m1" as UIMessageId; - const component = renderComponent(messageId, true, undefined, undefined, [ - "n1", - "n2", - "n3" - ]); + it("should match snapshot when not cancelled, with more-than-five (max-visible-payments) payable payments, without cancelled-completed-payments", () => { + const initialState = dsEnabledGlobalState(); + const paymentIds = [ + "01234567890012345678912345610", + "01234567890012345678912345620", + "01234567890012345678912345630", + "01234567890012345678912345640", + "01234567890012345678912345650", + "01234567890012345678912345660", + "01234567890012345678912345670" + ]; + const paymentsState: GlobalState = { + ...initialState, + entities: { + ...initialState.entities, + messages: { + ...initialState.entities.messages, + payments: { + ...initialState.entities.messages.payments, + [globalMessageId]: { + [paymentIds[0]]: remoteReady( + generatePayablePayment(paymentIds[0], 199) + ), + [paymentIds[1]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_ANNULLATO + ), + [paymentIds[2]]: remoteError(Detail_v2Enum.PAA_PAGAMENTO_SCADUTO), + [paymentIds[3]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_IN_CORSO + ), + [paymentIds[4]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + ), + [paymentIds[5]]: remoteError( + Detail_v2Enum.PAA_PAGAMENTO_SCONOSCIUTO + ), + [paymentIds[6]]: remoteError(Detail_v2Enum.GENERIC_ERROR) + } + } + } + } + }; + const payments = notificationPaymentInfosFromPaymentIds(paymentIds); + const component = renderComponent( + globalMessageId, + false, + payments, + [], + paymentsState + ); expect(component.toJSON()).toMatchSnapshot(); }); }); +const dsEnabledGlobalState = (): GlobalState => { + const initialState = appReducer(undefined, applicationChangeState("active")); + return appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); +}; + const renderComponent = ( messageId: UIMessageId, isCancelled: boolean, - payments: ReadonlyArray | undefined, - paymentsStatus: "payable" | "processed" | undefined, - completedPaymentNoticeCodes: ReadonlyArray | undefined + payments: Array, + completedPayments: Array, + initialState: GlobalState, + maxVisiblePaymentCount: number = globalMaxVisiblePaymentCount ) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const modifiedState = - payments?.reduce( - (previousState, payment) => - paymentsStatus === "payable" - ? appReducer( - previousState, - updatePaymentForMessage.success({ - messageId, - paymentId: ``, - paymentData: { - codiceContestoPagamento: `${payment.noticeCode}`, - importoSingoloVersamento: 99, - causaleVersamento: "Causale", - dueDate: new Date(2023, 10, 23, 10, 30) - } as PaymentRequestsGetResponse - }) - ) - : paymentsStatus === "processed" - ? appReducer( - previousState, - updatePaymentForMessage.failure({ - messageId, - paymentId: ``, - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }) - ) - : previousState, - globalState - ) ?? globalState; - const store = createStore(appReducer, modifiedState as any); - + const store = createStore(appReducer, initialState as any); const mockPresentPaymentsBottomSheetRef = { current: () => undefined }; @@ -232,15 +485,15 @@ const renderComponent = ( return renderScreenWithNavigationStoreContext( () => ( ), - "DUMMY", + PN_ROUTES.MESSAGE_DETAILS, {}, store ); diff --git a/ts/features/pn/components/__test__/__snapshots__/F24ListBottomSheetLink.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/F24ListBottomSheetLink.test.tsx.snap new file mode 100644 index 00000000000..aa3ab17c2dd --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/F24ListBottomSheetLink.test.tsx.snap @@ -0,0 +1,3955 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`F24ListBottomSheetLink should be snapshot for a 10 items F24 list 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + Vedi modelli F24 + + + + + + + + + + + + 0 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 1 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 2 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 3 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 4 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 5 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 6 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 7 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 8 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 9 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24ListBottomSheetLink should be snapshot for an 0 items F24 list 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + Vedi modelli F24 + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24ListBottomSheetLink should be snapshot for an 1 item F24 list 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + Vedi modelli F24 + + + + + + + + + + + + 0 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/F24Section.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/F24Section.test.tsx.snap new file mode 100644 index 00000000000..290dbb64d72 --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/F24Section.test.tsx.snap @@ -0,0 +1,3242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`F24Section should match snapshot when there are more than one F24 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Modelli F24 + + + + + + + Se preferisci, puoi pagare questa notifica tramite F24. + + + + + + + Vedi modelli F24 + + + + + + + + + + + + 2 + + + + + + PDF + + + + + + + + + + + + + + + + + + + 4 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24Section should match snapshot when there are more than one F24 and the message is cancelled 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24Section should match snapshot when there are no F24 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24Section should match snapshot when there are no F24 and the message is cancelled 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24Section should match snapshot when there is a single F24 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Modelli F24 + + + + + + + Se preferisci, puoi pagare questa notifica tramite F24. + + + + + + + 2 + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`F24Section should match snapshot when there is a single F24 and the message is cancelled 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/LegacyMessageF24.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/LegacyMessageF24.test.tsx.snap new file mode 100644 index 00000000000..84c0914f01c --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/LegacyMessageF24.test.tsx.snap @@ -0,0 +1,1213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageF24 component when there are multiple F24 should match the snapshot 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Modelli F24 + + + + + + Se preferisci, puoi pagare questa notifica tramite F24. + + + + + + + Vedi modelli F24 + + + + + + + + + + + + + + + + + +`; + +exports[`MessageF24 component when there is only one F24 should match the snapshot 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Modelli F24 + + + + + + Se preferisci, puoi pagare questa notifica tramite F24. + + + + + + + + + + + + + + + invoice.pdf + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/LegacyMessageFooter.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/LegacyMessageFooter.test.tsx.snap new file mode 100644 index 00000000000..b90a546a1a2 --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/LegacyMessageFooter.test.tsx.snap @@ -0,0 +1,1788 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LegacyMessageFooter should match snapshot for cancelled PN notification 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessageFooter should match snapshot for hidden button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessageFooter should match snapshot for visibleEnabled button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + Go to payment + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessageFooter should match snapshot for visibleLoading button 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/LegacyMessagePayments.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/LegacyMessagePayments.test.tsx.snap new file mode 100644 index 00000000000..c6b7026b617 --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/LegacyMessagePayments.test.tsx.snap @@ -0,0 +1,6428 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LegacyMessagePayments component Should match the snapshot for a cancelled message with no completed payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for a cancelled message with three completed payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Pagamenti + + + + + + + + + + + + + + + + Per l'eventuale rimborso di pagamenti legati a questa notifica, contatta l'ente mittente. + + + + + + + + + + Codice avviso + + + n1 + + + + + + Paid + + + + + + + + + + + + + + + + + + Codice avviso + + + n2 + + + + + + Paid + + + + + + + + + + + + + + + + + + Codice avviso + + + n3 + + + + + + Paid + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for a single loading payment 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for five loading payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for five payable payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for five processed payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyMessagePayments component Should match the snapshot for more than five loading payments 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessageDetails.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessageDetails.test.tsx.snap index ec8a030d60f..6f069dee2a8 100644 --- a/ts/features/pn/components/__test__/__snapshots__/MessageDetails.test.tsx.snap +++ b/ts/features/pn/components/__test__/__snapshots__/MessageDetails.test.tsx.snap @@ -20,637 +20,1081 @@ exports[`MessageDetails component should match the snapshot with default props 1 } > - - - + + /> + + - + - DUMMY_ROUTE - + + DUMMY_ROUTE + + + - - - - - + - - - - + + } + > + - - - + + + + + + + + + Legal value + + + + + + + - - + tintColor="#555C70" + vbHeight={24} + vbWidth={24} + width={16} + > + + + + + + - - Legal value - + + ######## subject ######## + + + 01 Jan 2020, 00:00 + + + + + + ######## abstract ######## + + + + + + + + + + + + + + + + + Informazioni sulla notifica + + + + + + + - - - + + Codice IUN + + - - + weight="SemiBold" + > + 731143-7-0317-8200-0 + + + + + + + + + + - - ######## subject ######## - - - - 01 Jan 2020, 00:00 - - - - - - - - ######## abstract ######## - + /> + - - + + - - + + diff --git a/ts/features/pn/components/__test__/__snapshots__/MessageDetailsContent.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessageDetailsContent.test.tsx.snap index 19f593ad32a..9b81a6c19e4 100644 --- a/ts/features/pn/components/__test__/__snapshots__/MessageDetailsContent.test.tsx.snap +++ b/ts/features/pn/components/__test__/__snapshots__/MessageDetailsContent.test.tsx.snap @@ -1,20 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MessageDetailsContent component should match the snapshot when abstract is defined 1`] = ` - +Array [ + />, abstract - - + , +] `; exports[`MessageDetailsContent component should match the snapshot when abstract is not defined 1`] = `null`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessageF24.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessageF24.test.tsx.snap deleted file mode 100644 index b044f551d36..00000000000 --- a/ts/features/pn/components/__test__/__snapshots__/MessageF24.test.tsx.snap +++ /dev/null @@ -1,1120 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MessageF24 component when there are multiple F24 should match the snapshot 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - - Modelli F24 - - - - - - Se preferisci, puoi pagare questa notifica tramite F24. - - - - - - - Vedi modelli F24 - - - - - - - - - - - - - - - - -`; - -exports[`MessageF24 component when there is only one F24 should match the snapshot 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - - Modelli F24 - - - - - - Se preferisci, puoi pagare questa notifica tramite F24. - - - - - - - - - - - - - - - invoice.pdf - - - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessageFooter.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessageFooter.test.tsx.snap new file mode 100644 index 00000000000..524d260a84b --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/MessageFooter.test.tsx.snap @@ -0,0 +1,366 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageFooter should match snapshot 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessageInfo.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessageInfo.test.tsx.snap new file mode 100644 index 00000000000..02098919913 --- /dev/null +++ b/ts/features/pn/components/__test__/__snapshots__/MessageInfo.test.tsx.snap @@ -0,0 +1,671 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageInfo should match snapshot 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Informazioni sulla notifica + + + + + + + + + + + Codice IUN + + + YYYYMM-1-ABCD-EFGH-X + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessagePaymentItem.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessagePaymentItem.test.tsx.snap deleted file mode 100644 index 0256890a477..00000000000 --- a/ts/features/pn/components/__test__/__snapshots__/MessagePaymentItem.test.tsx.snap +++ /dev/null @@ -1,1499 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MessagePaymentItem component Should match the snapshot for a loading item 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`MessagePaymentItem component Should match the snapshot for a payable item 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - - - - - Due date 23/11/2023 - - - Causale - - - - - 0.99 € - - - - - - - - - - - - - - - - - - - - -`; - -exports[`MessagePaymentItem component Should match the snapshot for a processed item 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - - - - - Codice avviso - - - n1 - - - - - - Revoked - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/ts/features/pn/components/__test__/__snapshots__/MessagePayments.test.tsx.snap b/ts/features/pn/components/__test__/__snapshots__/MessagePayments.test.tsx.snap index acea4d2038d..edad042843f 100644 --- a/ts/features/pn/components/__test__/__snapshots__/MessagePayments.test.tsx.snap +++ b/ts/features/pn/components/__test__/__snapshots__/MessagePayments.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MessagePayments component Should match the snapshot for a cancelled message with no completed payments 1`] = ` +exports[`MessagePayments should match snapshot when cancelled with payments without completed payments 1`] = ` - - - - - - DUMMY - - + /> - - - - - - - + - - - + PN_ROUTES_MESSAGE_DETAILS + + - - - -`; - -exports[`MessagePayments component Should match the snapshot for a cancelled message with three completed payments 1`] = ` - - - - - @@ -360,1136 +212,314 @@ exports[`MessagePayments component Should match the snapshot for a cancelled mes collapsable={false} style={ Object { - "backgroundColor": "#FFFFFF", - "borderBottomColor": "rgb(216, 216, 216)", - "flex": 1, - "shadowColor": "rgb(216, 216, 216)", - "shadowOffset": Object { - "height": 0.5, - "width": 0, - }, - "shadowOpacity": 0.85, - "shadowRadius": 0, - } - } - /> - - - - - - DUMMY - - - - - - - - - - - - - - Pagamenti - - - - - - - - - - - - - - Per l'eventuale rimborso di pagamenti legati a questa notifica, contatta l'ente mittente. - - - - - - - - - Codice avviso - - - n1 - - - - - - Paid - - - - - - - - + Pagamenti + - - + + + - - - - - Codice avviso - - - n2 - - - - - - Paid - - - - - - - - - - - - - + + + + + - - - - - Codice avviso - - - n3 - - - - - - Paid - - - - - - - - - - + Per l'eventuale rimborso di pagamenti legati a questa notifica, contatta l'ente mittente. + @@ -1498,14 +528,14 @@ exports[`MessagePayments component Should match the snapshot for a cancelled mes - - + + `; -exports[`MessagePayments component Should match the snapshot for a single loading payment 1`] = ` +exports[`MessagePayments should match snapshot when cancelled, with payments, with cancelled-completed-payments 1`] = ` - - - + + /> + + - + - DUMMY - + + PN_ROUTES_MESSAGE_DETAILS + + + - - - - - + - - + - Avvisi pagoPA - + } + } + > + + + Pagamenti + + + + + + - - + } + > - - - - + + + style={ + Array [ + Object { + "fontSize": 16, + "lineHeight": 24, + }, + Object { + "color": "#555C70", + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "400", + }, + ] + } + testID="infoScreenBody" + weight="Regular" + > + Per l'eventuale rimborso di pagamenti legati a questa notifica, contatta l'ente mittente. + + + + + + - - - - + Codice avviso + + - - + + + + + > + Paid + + + + + + + + + + + + + - + Codice avviso + + + 01234567890012345678912345620 + + + + - + Paid + + + + - + > + + + - - - - - - - - - - - -`; - -exports[`MessagePayments component Should match the snapshot for five loading payments 1`] = ` - - - - - - - - - - - - - DUMMY - - - - - - - - - - - - - - - - - - - + - Avvisi pagoPA - - + - - - - - - - - - - - - - + Codice avviso + + - - - + 01234567890012345678912345630 + - + + > + Paid + + + + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345640 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345650 + - - - - + Array [ + Object { + "alignSelf": "center", + "flexShrink": 1, + "fontSize": 12, + "lineHeight": 16, + "textTransform": "uppercase", + }, + Object { + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "600", + }, + Object { + "color": "#224021", + }, + ] + } + > + Paid + + + + + + + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345660 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345670 + - + > + Paid + - + + + + + + @@ -3034,14 +2785,14 @@ exports[`MessagePayments component Should match the snapshot for five loading pa - - + + `; -exports[`MessagePayments component Should match the snapshot for five payable payments 1`] = ` +exports[`MessagePayments should match snapshot when cancelled, without payments, with cancelled-completed-payments 1`] = ` - - - + + /> + + - + - DUMMY - + + PN_ROUTES_MESSAGE_DETAILS + + + - - - - - + - - + - Avvisi pagoPA - + } + } + > + + + Pagamenti + + + + + + - - + } + > - - - - - + + + style={ + Array [ + Object { + "fontSize": 16, + "lineHeight": 24, + }, + Object { + "color": "#555C70", + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "400", + }, + ] + } + testID="infoScreenBody" + weight="Regular" + > + Per l'eventuale rimborso di pagamenti legati a questa notifica, contatta l'ente mittente. + + + + + + + - - - - + Codice avviso + + - - - + 01234567890012345678912345610 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345620 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345630 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - + Codice avviso + + - - - + 01234567890012345678912345640 + - + Paid + + + + - + > + + + - - + + + } + /> + - - - - - + Codice avviso + + + 01234567890012345678912345650 + + + + - + Paid + + + + - + > + + + + + + + + + + - + Codice avviso + + + 01234567890012345678912345660 + + + + - + Paid + + + + - + > + + + - - - - - - - - - - - -`; - -exports[`MessagePayments component Should match the snapshot for five processed payments 1`] = ` - - - - - - - - - - - - - DUMMY - + + + + + + + Codice avviso + + + 01234567890012345678912345670 + + + + + + Paid + + + + + + + + + + + + + + + - - + + - + +`; + +exports[`MessagePayments should match snapshot when cancelled, without payments, without cancelled-completed-payments 1`] = ` + + + - + + > + + + - - - + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, with five (max-visible-payments) payable payments, with cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + Due date 02/05/2099 + + + hendrerit orci id dolor consectetur + + + + + 1.99 € + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 20 + + + + + + Revoked + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 30 + + + + + + Expired + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 40 + + + + + + Ongoing + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 50 + + + + + + Paid + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, with five (max-visible-payments) payable payments, without cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + Due date 02/05/2099 + + + hendrerit orci id dolor consectetur + + + + + 1.99 € + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 20 + + + + + + Revoked + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 30 + + + + + + Expired + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 40 + + + + + + Ongoing + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 50 + + + + + + Paid + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, with more-than-five (max-visible-payments) payable payments, with cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + Due date 02/05/2099 + + + hendrerit orci id dolor consectetur + + + + + 1.99 € + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 20 + + + + + + Revoked + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 30 + + + + + + Expired + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 40 + + + + + + Ongoing + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 50 + + + + + + Paid + + + + + + + + + + + + + + + + + Vedi tutti gli avvisi (7) + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, with more-than-five (max-visible-payments) payable payments, without cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Avvisi pagoPA + + + + + + + + + + + Due date 02/05/2099 + + + hendrerit orci id dolor consectetur + + + + + 1.99 € + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 20 + + + + + + Revoked + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 30 + + + + + + Expired + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 40 + + + + + + Ongoing + + + + + + + + + + + + + + + + + + Notice code + + + 0123 4567 8912 3456 50 + + + + + + Paid + + + + + + + + + + + + + + + + + Vedi tutti gli avvisi (7) + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, with one payable payment, with cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + - - Avvisi pagoPA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - + Avvisi pagoPA + - - + + + - - - - + Due date 02/05/2099 + + - - - + hendrerit orci id dolor consectetur + - - + + - + > + + + @@ -5030,14 +13103,14 @@ exports[`MessagePayments component Should match the snapshot for five processed - - + + `; -exports[`MessagePayments component Should match the snapshot for more than five loading payments 1`] = ` +exports[`MessagePayments should match snapshot when not cancelled, with one payable payment, without cancelled-completed-payments 1`] = ` - - - + + /> + + - + - DUMMY - + + PN_ROUTES_MESSAGE_DETAILS + + + - - - - - + - - - - - - - Avvisi pagoPA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + } + > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - + Avvisi pagoPA + - - + + + - + - - - + Due date 02/05/2099 + + - - - + hendrerit orci id dolor consectetur + - - + + - + > + + + - - - - - - @@ -6066,8 +13819,698 @@ exports[`MessagePayments component Should match the snapshot for more than five + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, without payments, with cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessagePayments should match snapshot when not cancelled, without payments, without cancelled-completed-payments 1`] = ` + + + + + + + + + + + + + + + PN_ROUTES_MESSAGE_DETAILS + + + + + + + + + + + + + + + + + + + + + diff --git a/ts/features/pn/hooks/useF24BottomSheet.tsx b/ts/features/pn/hooks/useF24BottomSheet.tsx index dc0f4d8befc..5c1bcbf61e9 100644 --- a/ts/features/pn/hooks/useF24BottomSheet.tsx +++ b/ts/features/pn/hooks/useF24BottomSheet.tsx @@ -1,21 +1,24 @@ import React from "react"; -import { MessageAttachments } from "../../messages/components/MessageAttachments"; -import { UIAttachment } from "../../messages/types"; +import { LegacyMessageAttachments } from "../components/LegacyMessageAttachments"; import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet"; import I18n from "../../../i18n"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { UIMessageId } from "../../messages/types"; export const useF24BottomSheet = ( - attachments: ReadonlyArray, - openPreview: (attachment: UIAttachment) => void + attachments: ReadonlyArray, + messageId: UIMessageId, + openPreview: (attachment: ThirdPartyAttachment) => void ) => { const { present, bottomSheet, dismiss } = useIOBottomSheetAutoresizableModal( { component: ( - { + messageId={messageId} + openPreview={(attachment: ThirdPartyAttachment) => { dismiss(); openPreview(attachment); }} diff --git a/ts/features/pn/navigation/navigator.tsx b/ts/features/pn/navigation/navigator.tsx index d837d801971..b3ca54af98c 100644 --- a/ts/features/pn/navigation/navigator.tsx +++ b/ts/features/pn/navigation/navigator.tsx @@ -1,12 +1,14 @@ -import * as React from "react"; import { createStackNavigator } from "@react-navigation/stack"; +import * as React from "react"; +import { useIOSelector } from "../../../store/hooks"; +import { isDesignSystemEnabledSelector } from "../../../store/reducers/persistedPreferences"; import { isGestureEnabled } from "../../../utils/navigation"; -import { MessageDetailsScreen } from "../screens/MessageDetailsScreen"; +import { LegacyAttachmentPreviewScreen } from "../screens/LegacyAttachmentPreviewScreen"; import { LegacyMessageDetailsScreen } from "../screens/LegacyMessageDetailsScreen"; -import { AttachmentPreviewScreen } from "../screens/AttachmentPreviewScreen"; +import { MessageAttachmentScreen } from "../screens/MessageAttachmentScreen"; +import { MessageDetailsScreen } from "../screens/MessageDetailsScreen"; import { PaidPaymentScreen } from "../screens/PaidPaymentScreen"; -import { useIOSelector } from "../../../store/hooks"; -import { isDesignSystemEnabledSelector } from "../../../store/reducers/persistedPreferences"; +import { LegacyPaidPaymentScreen } from "../screens/LegacyPaidPaymentScreen"; import { PnParamsList } from "./params"; import PN_ROUTES from "./routes"; @@ -18,8 +20,11 @@ export const PnStackNavigator = () => { return ( { ? MessageDetailsScreen : LegacyMessageDetailsScreen } - options={{ - headerShown: isDesignSystemEnabled - }} /> ); diff --git a/ts/features/pn/navigation/params.ts b/ts/features/pn/navigation/params.ts index 02f0d6b39f0..a3c027d46d1 100644 --- a/ts/features/pn/navigation/params.ts +++ b/ts/features/pn/navigation/params.ts @@ -1,10 +1,10 @@ -import { AttachmentPreviewScreenNavigationParams } from "../screens/AttachmentPreviewScreen"; -import { MessageDetailsScreenNavigationParams } from "../screens/MessageDetailsScreen"; -import { PaidPaymentScreenNavigationParams } from "../screens/PaidPaymentScreen"; +import { MessageAttachmentScreenRouteParams } from "../screens/MessageAttachmentScreen"; +import { MessageDetailsScreenRouteParams } from "../screens/MessageDetailsScreen"; +import { PaidPaymentScreenRouteParams } from "../screens/PaidPaymentScreen"; import PN_ROUTES from "./routes"; export type PnParamsList = { - [PN_ROUTES.MESSAGE_DETAILS]: MessageDetailsScreenNavigationParams; - [PN_ROUTES.MESSAGE_ATTACHMENT]: AttachmentPreviewScreenNavigationParams; - [PN_ROUTES.CANCELLED_MESSAGE_PAID_PAYMENT]: PaidPaymentScreenNavigationParams; + [PN_ROUTES.MESSAGE_DETAILS]: MessageDetailsScreenRouteParams; + [PN_ROUTES.MESSAGE_ATTACHMENT]: MessageAttachmentScreenRouteParams; + [PN_ROUTES.CANCELLED_MESSAGE_PAID_PAYMENT]: PaidPaymentScreenRouteParams; }; diff --git a/ts/features/pn/screens/AttachmentPreviewScreen.tsx b/ts/features/pn/screens/LegacyAttachmentPreviewScreen.tsx similarity index 68% rename from ts/features/pn/screens/AttachmentPreviewScreen.tsx rename to ts/features/pn/screens/LegacyAttachmentPreviewScreen.tsx index e516603718a..98611b7ce96 100644 --- a/ts/features/pn/screens/AttachmentPreviewScreen.tsx +++ b/ts/features/pn/screens/LegacyAttachmentPreviewScreen.tsx @@ -3,7 +3,6 @@ import { pipe } from "fp-ts/lib/function"; import * as B from "fp-ts/lib/boolean"; import * as O from "fp-ts/lib/Option"; import { PnParamsList } from "../navigation/params"; -import { UIMessageId, UIAttachmentId } from "../../messages/types"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../store/hooks"; import { pnMessageAttachmentSelector } from "../store/reducers"; @@ -17,22 +16,16 @@ import { import { isIos } from "../../../utils/platform"; import { LegacyMessageAttachmentPreview } from "../../messages/components/MessageAttachment/LegacyMessageAttachmentPreview"; -export type AttachmentPreviewScreenNavigationParams = Readonly<{ - messageId: UIMessageId; - attachmentId: UIAttachmentId; - category?: string; -}>; - -type AttachmentPreviewScreenProps = IOStackNavigationRouteProps< +type LegacyAttachmentPreviewScreenProps = IOStackNavigationRouteProps< PnParamsList, "PN_ROUTES_MESSAGE_ATTACHMENT" >; -export const AttachmentPreviewScreen = ({ +export const LegacyAttachmentPreviewScreen = ({ navigation, route -}: AttachmentPreviewScreenProps) => { - const { messageId, attachmentId, category } = route.params; +}: LegacyAttachmentPreviewScreenProps) => { + const { messageId, attachmentId } = route.params; // This ref is needed otherwise the auto back on the useEffect will fire multiple // times, since its dependencies change during the back navigation const autoBackOnErrorHandled = useRef(false); @@ -55,21 +48,37 @@ export const AttachmentPreviewScreen = ({ messageId={messageId} enableDownloadAttachment={false} attachment={maybePnMessageAttachment.value} - onOpen={() => trackPNAttachmentOpen(category)} + onOpen={() => + trackPNAttachmentOpen(maybePnMessageAttachment.value.category) + } onShare={() => pipe( isIos, B.fold( - () => trackPNAttachmentShare(category), - () => trackPNAttachmentSaveShare(category) + () => + trackPNAttachmentShare(maybePnMessageAttachment.value.category), + () => + trackPNAttachmentSaveShare( + maybePnMessageAttachment.value.category + ) ) ) } - onDownload={() => trackPNAttachmentSave(category)} + onDownload={() => + trackPNAttachmentSave(maybePnMessageAttachment.value.category) + } onLoadComplete={() => - trackPNAttachmentOpeningSuccess("displayer", category) + trackPNAttachmentOpeningSuccess( + "displayer", + maybePnMessageAttachment.value.category + ) + } + onPDFError={() => + trackPNAttachmentOpeningSuccess( + "error", + maybePnMessageAttachment.value.category + ) } - onPDFError={() => trackPNAttachmentOpeningSuccess("error", category)} /> ) : ( <> diff --git a/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx b/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx index 7d2ad7d08a2..8343d077bb3 100644 --- a/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx +++ b/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx @@ -4,19 +4,25 @@ import * as O from "fp-ts/lib/Option"; import React from "react"; import { SafeAreaView } from "react-native"; import { useFocusEffect, useNavigation } from "@react-navigation/native"; -import { useStore } from "react-redux"; import { IOStyles } from "../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import I18n from "../../../i18n"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../store/hooks"; -import { serviceByIdSelector } from "../../../store/reducers/entities/services/servicesById"; +import { useIODispatch, useIOSelector, useIOStore } from "../../../store/hooks"; +import { serviceByIdPotSelector } from "../../services/store/reducers/servicesById"; import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; import { LegacyMessageDetails } from "../components/LegacyMessageDetails"; import { PnParamsList } from "../navigation/params"; -import { pnMessageFromIdSelector } from "../store/reducers"; -import { cancelPreviousAttachmentDownload } from "../../messages/store/actions"; +import { + pnMessageFromIdSelector, + pnUserSelectedPaymentRptIdSelector +} from "../store/reducers"; +import { + cancelPreviousAttachmentDownload, + cancelQueuedPaymentUpdates, + updatePaymentForMessage +} from "../../messages/store/actions"; import { profileFiscalCodeSelector } from "../../../store/reducers/profile"; import { containsF24FromPNMessagePot, @@ -27,13 +33,8 @@ import { trackPNUxSuccess } from "../analytics"; import { isStrictSome } from "../../../utils/pot"; import { cancelPaymentStatusTracking, - cancelQueuedPaymentUpdates, - clearSelectedPayment, - startPaymentStatusTracking, - updatePaymentForMessage + startPaymentStatusTracking } from "../store/actions"; -import { GlobalState } from "../../../store/reducers/types"; -import { selectedPaymentIdSelector } from "../store/reducers/payments"; import { InfoScreenComponent } from "../../../components/infoScreen/InfoScreenComponent"; import { renderInfoRasterImage } from "../../../components/infoScreen/imageRendering"; import genericErrorIcon from "../../../../img/wallet/errors/generic-error-icon.png"; @@ -47,7 +48,7 @@ export const LegacyMessageDetailsScreen = ( const navigation = useNavigation(); const service = pot.toUndefined( - useIOSelector(state => serviceByIdSelector(state, serviceId)) + useIOSelector(state => serviceByIdPotSelector(state, serviceId)) ); const currentFiscalCode = useIOSelector(profileFiscalCodeSelector); @@ -80,21 +81,23 @@ export const LegacyMessageDetailsScreen = ( } }); - const store = useStore(); + const store = useIOStore(); useFocusEffect( React.useCallback(() => { - const globalState = store.getState() as GlobalState; - const selectedPaymentId = selectedPaymentIdSelector(globalState); - if (selectedPaymentId) { - dispatch(clearSelectedPayment()); + const globalState = store.getState(); + const paymentToCheckRptId = pnUserSelectedPaymentRptIdSelector( + globalState, + messagePot + ); + if (paymentToCheckRptId) { dispatch( updatePaymentForMessage.request({ messageId, - paymentId: selectedPaymentId + paymentId: paymentToCheckRptId }) ); } - }, [dispatch, messageId, store]) + }, [dispatch, messageId, messagePot, store]) ); return ( diff --git a/ts/features/pn/screens/LegacyPaidPaymentScreen.tsx b/ts/features/pn/screens/LegacyPaidPaymentScreen.tsx new file mode 100644 index 00000000000..0a77d85d876 --- /dev/null +++ b/ts/features/pn/screens/LegacyPaidPaymentScreen.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { SafeAreaView, ScrollView } from "react-native"; +import { ListItemInfoCopy } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import I18n from "../../../i18n"; +import { IOStyles } from "../../../components/core/variables/IOStyles"; +import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; +import { TransactionSummaryStatus } from "../../../screens/wallet/payment/components/TransactionSummaryStatus"; +import { clipboardSetStringWithFeedback } from "../../../utils/clipboard"; +import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; +import { TransactionSummaryError } from "../../../screens/wallet/payment/TransactionSummaryScreen"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { PnParamsList } from "../navigation/params"; + +type LegacyPaidPaymentScreenProps = IOStackNavigationRouteProps< + PnParamsList, + "PN_CANCELLED_MESSAGE_PAID_PAYMENT" +>; + +const paidPaymentError = O.some( + "PPT_PAGAMENTO_DUPLICATO" +) as TransactionSummaryError; + +export const LegacyPaidPaymentScreen = ({ + route +}: LegacyPaidPaymentScreenProps) => { + const { noticeCode, creditorTaxId } = route.params; + const formattedPaymentNoticeNumber = noticeCode + .replace(/(\d{4})/g, "$1 ") + .trim(); + + return ( + + + + + clipboardSetStringWithFeedback(noticeCode)} + /> + {creditorTaxId && ( + clipboardSetStringWithFeedback(creditorTaxId)} + /> + )} + + + + ); +}; diff --git a/ts/features/pn/screens/MessageAttachmentScreen.tsx b/ts/features/pn/screens/MessageAttachmentScreen.tsx new file mode 100644 index 00000000000..7e1e4ce10b1 --- /dev/null +++ b/ts/features/pn/screens/MessageAttachmentScreen.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { UIMessageId } from "../../messages/types"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { PnParamsList } from "../navigation/params"; +import { MessageAttachment } from "../../messages/components/MessageAttachment/MessageAttachment"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; + +export type MessageAttachmentScreenRouteParams = Readonly<{ + messageId: UIMessageId; + attachmentId: string; +}>; + +type MessageAttachmentScreenProps = IOStackNavigationRouteProps< + PnParamsList, + "PN_ROUTES_MESSAGE_ATTACHMENT" +>; + +export const MessageAttachmentScreen = ( + props: MessageAttachmentScreenProps +) => { + const { attachmentId, messageId } = props.route.params; + + useHeaderSecondLevel({ + title: "", + supportRequest: true + }); + + return ( + + ); +}; diff --git a/ts/features/pn/screens/MessageDetailsScreen.tsx b/ts/features/pn/screens/MessageDetailsScreen.tsx index dc68816b0b1..419fb72b572 100644 --- a/ts/features/pn/screens/MessageDetailsScreen.tsx +++ b/ts/features/pn/screens/MessageDetailsScreen.tsx @@ -5,20 +5,25 @@ import { useNavigation, useRoute } from "@react-navigation/native"; -import { useStore } from "react-redux"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import I18n from "../../../i18n"; -import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { useIODispatch, useIOSelector, useIOStore } from "../../../store/hooks"; import { UIMessageId } from "../../messages/types"; -import { serviceByIdSelector } from "../../../store/reducers/entities/services/servicesById"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; import { MessageDetails } from "../components/MessageDetails"; import { PnParamsList } from "../navigation/params"; -import { pnMessageFromIdSelector } from "../store/reducers"; -import { cancelPreviousAttachmentDownload } from "../../messages/store/actions"; +import { + pnMessageFromIdSelector, + pnUserSelectedPaymentRptIdSelector +} from "../store/reducers"; +import { + cancelPreviousAttachmentDownload, + cancelQueuedPaymentUpdates, + updatePaymentForMessage +} from "../../messages/store/actions"; import { profileFiscalCodeSelector } from "../../../store/reducers/profile"; import { containsF24FromPNMessagePot, @@ -29,17 +34,12 @@ import { trackPNUxSuccess } from "../analytics"; import { isStrictSome } from "../../../utils/pot"; import { cancelPaymentStatusTracking, - cancelQueuedPaymentUpdates, - clearSelectedPayment, - startPaymentStatusTracking, - updatePaymentForMessage + startPaymentStatusTracking } from "../store/actions"; -import { GlobalState } from "../../../store/reducers/types"; -import { selectedPaymentIdSelector } from "../store/reducers/payments"; import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; -export type MessageDetailsScreenNavigationParams = { +export type MessageDetailsScreenRouteParams = { messageId: UIMessageId; serviceId: ServiceId; firstTimeOpening: boolean; @@ -57,9 +57,6 @@ export const MessageDetailsScreen = () => { const { messageId, serviceId, firstTimeOpening } = route.params; - const service = pot.toUndefined( - useIOSelector(state => serviceByIdSelector(state, serviceId)) - ); const currentFiscalCode = useIOSelector(profileFiscalCodeSelector); const messagePot = useIOSelector(state => pnMessageFromIdSelector(state, messageId) @@ -96,21 +93,23 @@ export const MessageDetailsScreen = () => { } }); - const store = useStore(); + const store = useIOStore(); useFocusEffect( useCallback(() => { - const globalState = store.getState() as GlobalState; - const selectedPaymentId = selectedPaymentIdSelector(globalState); - if (selectedPaymentId) { - dispatch(clearSelectedPayment()); + const globalState = store.getState(); + const paymentToCheckRptId = pnUserSelectedPaymentRptIdSelector( + globalState, + messagePot + ); + if (paymentToCheckRptId) { dispatch( updatePaymentForMessage.request({ messageId, - paymentId: selectedPaymentId + paymentId: paymentToCheckRptId }) ); } - }, [dispatch, messageId, store]) + }, [dispatch, messageId, messagePot, store]) ); return ( @@ -129,9 +128,9 @@ export const MessageDetailsScreen = () => { ), message => ( ) diff --git a/ts/features/pn/screens/PaidPaymentScreen.tsx b/ts/features/pn/screens/PaidPaymentScreen.tsx index e65c6afbcbc..a23e5ae4d6f 100644 --- a/ts/features/pn/screens/PaidPaymentScreen.tsx +++ b/ts/features/pn/screens/PaidPaymentScreen.tsx @@ -1,71 +1,38 @@ import React from "react"; -import * as O from "fp-ts/lib/Option"; -import I18n from "i18n-js"; -import { SafeAreaView, ScrollView, StyleSheet } from "react-native"; -import { ListItemInfoCopy } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import { TransactionSummaryStatus } from "../../../screens/wallet/payment/components/TransactionSummaryStatus"; -import { clipboardSetStringWithFeedback } from "../../../utils/clipboard"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import { PnParamsList } from "../navigation/params"; -import customVariables from "../../../theme/variables"; -import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; -import { TransactionSummaryError } from "../../../screens/wallet/payment/TransactionSummaryScreen"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../../navigation/params/AppParamsList"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; +import I18n from "../../../i18n"; -const styles = StyleSheet.create({ - container: { - paddingHorizontal: customVariables.contentPadding - } -}); - -export type PaidPaymentScreenNavigationParams = Readonly<{ +export type PaidPaymentScreenRouteParams = { noticeCode: string; creditorTaxId?: string; -}>; - -const paidPaymentError = O.some( - "PPT_PAGAMENTO_DUPLICATO" -) as TransactionSummaryError; +}; -export const PaidPaymentScreen = ( - props: IOStackNavigationRouteProps< - PnParamsList, - "PN_CANCELLED_MESSAGE_PAID_PAYMENT" - > -): React.ReactElement => { - const { noticeCode, creditorTaxId: maybeCreditorTaxId } = props.route.params; - const formattedPaymentNoticeNumber = noticeCode - .replace(/(\d{4})/g, "$1 ") - .trim(); +type PaidPaymentScreenProps = IOStackNavigationRouteProps< + PnParamsList, + "PN_CANCELLED_MESSAGE_PAID_PAYMENT" +>; +export const PaidPaymentScreen = (_: PaidPaymentScreenProps) => { + const navigation = useIONavigation(); + useHeaderSecondLevel({ + title: "", + supportRequest: true + }); return ( - - - - - clipboardSetStringWithFeedback(noticeCode)} - /> - {maybeCreditorTaxId && ( - clipboardSetStringWithFeedback(maybeCreditorTaxId)} - /> - )} - - - + navigation.pop() + }} + /> ); }; diff --git a/ts/features/pn/screens/__test__/LegacyPaidPaymentScreen.test.tsx b/ts/features/pn/screens/__test__/LegacyPaidPaymentScreen.test.tsx new file mode 100644 index 00000000000..0d16f445ecd --- /dev/null +++ b/ts/features/pn/screens/__test__/LegacyPaidPaymentScreen.test.tsx @@ -0,0 +1,39 @@ +import { cleanup } from "@testing-library/react-native"; +import configureMockStore from "redux-mock-store"; +import { LegacyPaidPaymentScreen } from "../LegacyPaidPaymentScreen"; +import PN_ROUTES from "../../navigation/routes"; +import { GlobalState } from "../../../../store/reducers/types"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; + +describe("LegacyPaidPaymentScreen", () => { + // Needed to avoid `ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.` + afterEach(cleanup); + it("should render with back button, title, help button, paid banner, notice code and creditor tax id", () => { + const component = generateComponent( + generateNoticeCode(), + generateCreditorTaxId() + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("should render with back button, title, help button, paid banner and notice code but no creditor tax id", () => { + const component = generateComponent(generateNoticeCode()); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const generateNoticeCode = () => "018011988086479497"; +const generateCreditorTaxId = () => "00000000009"; + +const generateComponent = (noticeCode: string, creditorTaxId?: string) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const store = configureMockStore()(globalState); + + return renderScreenWithNavigationStoreContext( + LegacyPaidPaymentScreen, + PN_ROUTES.CANCELLED_MESSAGE_PAID_PAYMENT, + { noticeCode, creditorTaxId }, + store + ); +}; diff --git a/ts/features/pn/screens/__test__/PaidPaymentScreen.test.tsx b/ts/features/pn/screens/__test__/PaidPaymentScreen.test.tsx index 89eabe93e55..064f5efd260 100644 --- a/ts/features/pn/screens/__test__/PaidPaymentScreen.test.tsx +++ b/ts/features/pn/screens/__test__/PaidPaymentScreen.test.tsx @@ -1,54 +1,31 @@ -import * as React from "react"; -import { createStackNavigator } from "@react-navigation/stack"; -import renderer from "react-test-renderer"; -import { cleanup } from "@testing-library/react-native"; -import { NavigationContainer } from "@react-navigation/native"; -import configureMockStore from "redux-mock-store"; -import { Provider } from "react-redux"; -import { PaidPaymentScreen } from "../PaidPaymentScreen"; +import { createStore } from "redux"; +import { appReducer } from "../../../../store/reducers"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; import PN_ROUTES from "../../navigation/routes"; +import { PaidPaymentScreen } from "../PaidPaymentScreen"; import { GlobalState } from "../../../../store/reducers/types"; -import { applicationChangeState } from "../../../../store/actions/application"; -import { appReducer } from "../../../../store/reducers"; describe("PaidPaymentScreen", () => { - // Needed to avoid `ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.` - afterEach(cleanup); - it("should render with back button, title, help button, paid banner, notice code and creditor tax id", () => { - const tree = renderer - .create(generateComponent(generateNoticeCode(), generateCreditorTaxId())) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - it("should render with back button, title, help button, paid banner and notice code but no creditor tax id", () => { - const tree = renderer - .create(generateComponent(generateNoticeCode())) - .toJSON(); - expect(tree).toMatchSnapshot(); + it("should match snapshot", () => { + const screen = renderScreen(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); -const generateNoticeCode = () => "018011988086479497"; -const generateCreditorTaxId = () => "00000000009"; +const renderScreen = () => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); -const generateComponent = (noticeCode: string, creditorTaxId?: string) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const store = configureMockStore()(globalState); - const Stack = createStackNavigator(); - return ( - - - - - - - + return renderScreenWithNavigationStoreContext( + PaidPaymentScreen, + PN_ROUTES.CANCELLED_MESSAGE_PAID_PAYMENT, + { noticeCode: "012345678912345670", creditorTaxId: "01234567890" }, + store ); }; diff --git a/ts/features/pn/screens/__test__/__snapshots__/LegacyPaidPaymentScreen.test.tsx.snap b/ts/features/pn/screens/__test__/__snapshots__/LegacyPaidPaymentScreen.test.tsx.snap new file mode 100644 index 00000000000..3042d1789d4 --- /dev/null +++ b/ts/features/pn/screens/__test__/__snapshots__/LegacyPaidPaymentScreen.test.tsx.snap @@ -0,0 +1,2266 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LegacyPaidPaymentScreen should render with back button, title, help button, paid banner and notice code but no creditor tax id 1`] = ` + + + + + + + + + + + + + + + PN_CANCELLED_MESSAGE_PAID_PAYMENT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Payment informations + + + + + + + + + + + + + + + + + + + + + + + + + This notice has been already paid! + + + + + + + + + + + Notice code + + + 0180 1198 8086 4794 97 + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`LegacyPaidPaymentScreen should render with back button, title, help button, paid banner, notice code and creditor tax id 1`] = ` + + + + + + + + + + + + + + + PN_CANCELLED_MESSAGE_PAID_PAYMENT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Payment informations + + + + + + + + + + + + + + + + + + + + + + + + + This notice has been already paid! + + + + + + + + + + + Notice code + + + 0180 1198 8086 4794 97 + + + + + + + + + + + + + + + + + + Payee Fiscal Code + + + 00000000009 + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/pn/screens/__test__/__snapshots__/MessageDetailsScreen.test.tsx.snap b/ts/features/pn/screens/__test__/__snapshots__/MessageDetailsScreen.test.tsx.snap index 4323d0c8345..eae5108fcd2 100644 --- a/ts/features/pn/screens/__test__/__snapshots__/MessageDetailsScreen.test.tsx.snap +++ b/ts/features/pn/screens/__test__/__snapshots__/MessageDetailsScreen.test.tsx.snap @@ -20,954 +20,2003 @@ exports[`MessageDetailsScreen should match the snapshot when everything went fin } > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + + } + > + - - - + + + + + + + + + Legal value + + + + + + + - - + tintColor="#555C70" + vbHeight={24} + vbWidth={24} + width={16} + > + + + + + + - - Legal value - - + ######## subject ######## + + + + 01 Jan 2020, 00:00 + + + + - + Ċentru tas-Saħħa + + - - + + + + + - - + + - - - - ######## subject ######## - - - + + - 01 Jan 2020, 00:00 - - - - + ######## abstract ######## + + + /> - - Ċentru tas-Saħħa - - + + + + + + + + - health - + "flex": 1, + } + } + > + + + Attachments + + + + - + + A First Attachment + + + + + + PDF + + + + + + > + + + + + + + + + + + + + + A Second Attachment + + + + + + PDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Informazioni sulla notifica + + + + + + + + + + + Codice IUN + + + 731143-7-0317-8200-0 + + + + + + + + + + + > + + + + + + - - ######## abstract ######## - + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + @@ -993,718 +2042,815 @@ exports[`MessageDetailsScreen should match the snapshot when there is an error 1 } > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + Qualcosa è andato storto + + + Non è stato possibile recuperare i dettagli del tuo messaggio. Riprova per favore + - + + + + + - Qualcosa è andato storto - - - + - Non è stato possibile recuperare i dettagli del tuo messaggio. Riprova per favore - + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + diff --git a/ts/features/pn/screens/__test__/__snapshots__/PaidPaymentScreen.test.tsx.snap b/ts/features/pn/screens/__test__/__snapshots__/PaidPaymentScreen.test.tsx.snap index 8af2ce68b75..032cdb73248 100644 --- a/ts/features/pn/screens/__test__/__snapshots__/PaidPaymentScreen.test.tsx.snap +++ b/ts/features/pn/screens/__test__/__snapshots__/PaidPaymentScreen.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PaidPaymentScreen should render with back button, title, help button, paid banner and notice code but no creditor tax id 1`] = ` +exports[`PaidPaymentScreen should match snapshot 1`] = ` - - @@ -54,312 +71,415 @@ exports[`PaidPaymentScreen should render with back button, title, help button, p collapsable={false} style={ Object { - "backgroundColor": "rgb(255, 255, 255)", - "borderBottomColor": "rgb(216, 216, 216)", - "flex": 1, - "shadowColor": "rgb(216, 216, 216)", - "shadowOffset": Object { - "height": 0.5, - "width": 0, - }, - "shadowOpacity": 0.85, - "shadowRadius": 0, - } - } - /> - - - - - - PN_CANCELLED_MESSAGE_PAID_PAYMENT - - - - - - - - - - - - - - - + + + + > + + + + + + + + + + + + + + + + + + Questo avviso è stato già pagato! + + /> + - - - - - - - - - - - - Payment informations - - - - - - - - - - - - - - - - - - - - - - - - - This notice has been already paid! - - - - - - - - - - - Notice code - - - 0180 1198 8086 4794 97 - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`PaidPaymentScreen should render with back button, title, help button, paid banner, notice code and creditor tax id 1`] = ` - - - - - - - - - - - - - PN_CANCELLED_MESSAGE_PAID_PAYMENT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Payment informations - - - - - - - - - - - - - - - - - - - - - + } + importantForAccessibility="no-hide-descendants" + maxFontSizeMultiplier={1.3} + numberOfLines={1} + style={ + Array [ + Object { + "alignSelf": "center", + }, + Object { + "fontSize": 16, + }, + Object { + "color": "#FFFFFF", + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "700", + }, + ] + } + weight="Bold" + > + Close + + + + + + - + + + + + + - - This notice has been already paid! - - - - - - - - - - Notice code - - - 0180 1198 8086 4794 97 - - - - - - - - - - + /> + + - + + + - - - Payee Fiscal Code - - - 00000000009 - - - - - - - - - + } + propList={ + Array [ + "fill", + ] + } + /> + + - - + + - - + + + diff --git a/ts/features/pn/store/actions/index.ts b/ts/features/pn/store/actions/index.ts index 76d2415b795..7d4a6f6a774 100644 --- a/ts/features/pn/store/actions/index.ts +++ b/ts/features/pn/store/actions/index.ts @@ -1,27 +1,5 @@ import { ActionType, createAction, createAsyncAction } from "typesafe-actions"; import { UIMessageId } from "../../../messages/types"; -import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; -import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; - -export type UpdatePaymentForMessageRequest = { - messageId: UIMessageId; - paymentId: string; -}; - -export type UpdatePaymentForMessageSuccess = { - messageId: UIMessageId; - paymentId: string; - paymentData: PaymentRequestsGetResponse; -}; - -export type UpdatePaymentForMessageFailure = { - messageId: UIMessageId; - paymentId: string; - details: Detail_v2Enum; -}; - -export type UpdatePaymentForMessageCancel = - ReadonlyArray; export const pnActivationUpsert = createAsyncAction( "PN_ACTIVATION_UPSERT_REQUEST", @@ -29,42 +7,15 @@ export const pnActivationUpsert = createAsyncAction( "PN_ACTIVATION_UPSERT_FAILURE" )(); -export const updatePaymentForMessage = createAsyncAction( - "UPDATE_PAYMENT_FOR_MESSAGE_REQUEST", - "UPDATE_PAYMENT_FOR_MESSAGE_SUCCESS", - "UPDATE_PAYMENT_FOR_MESSAGE_FAILURE", - "UPDATE_PAYMENT_FOR_MESSAGE_CANCEL" -)< - UpdatePaymentForMessageRequest, - UpdatePaymentForMessageSuccess, - UpdatePaymentForMessageFailure, - UpdatePaymentForMessageCancel ->(); - -export const cancelQueuedPaymentUpdates = createAction( - "CANCEL_QUEUED_PAYMENT_UPDATES" -); - -export const setSelectedPayment = createAction( - "PN_SET_SELECTED_PAYMENT", - resolve => (paymentId: string) => resolve({ paymentId }) -); - -export const clearSelectedPayment = createAction("PN_CLEAR_SELECTED_PAYMENT"); - export const startPaymentStatusTracking = createAction( - "PN_START_TRACKING_PAYMENT_STATUS", + "MESSAGES_START_TRACKING_PAYMENT_STATUS", resolve => (messageId: UIMessageId) => resolve({ messageId }) ); export const cancelPaymentStatusTracking = createAction( - "PN_CANCEL_PAYMENT_STATUS_TRACKING" + "MESSAGES_CANCEL_PAYMENT_STATUS_TRACKING" ); export type PnActions = | ActionType - | ActionType - | ActionType - | ActionType - | ActionType | ActionType | ActionType; diff --git a/ts/features/pn/store/reducers/__test__/index.test.ts b/ts/features/pn/store/reducers/__test__/index.test.ts new file mode 100644 index 00000000000..65d243df753 --- /dev/null +++ b/ts/features/pn/store/reducers/__test__/index.test.ts @@ -0,0 +1,143 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pnUserSelectedPaymentRptIdSelector } from ".."; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { PNMessage } from "../../types/types"; +import { GlobalState } from "../../../../../store/reducers/types"; + +describe("pnUserSelectedPaymentRptIdSelector", () => { + const p3CreditorTaxId = "01234567890"; + const p3NoticeCode = "012345678912345630"; + const pnMessage = { + recipients: [ + { + payment: { + noticeCode: "012345678912345610", + creditorTaxId: "01234567890" + } + }, + { + payment: { + noticeCode: "012345678912345620", + creditorTaxId: "01234567890" + } + }, + { + payment: { + noticeCode: p3NoticeCode, + creditorTaxId: p3CreditorTaxId + } + } + ] + }; + const maybePNMessage = O.some(pnMessage) as O.Option; + const pnMessagePot = pot.some(maybePNMessage) as pot.Pot< + O.Option, + Error + >; + it("should return undefined when the pot is none", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.none; + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when the pot is noneLoading", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.noneLoading; + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when the pot is noneUpdating", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.noneUpdating(maybePNMessage); + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when the pot is noneError", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.noneError(new Error()); + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when the pot some with Option.None", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.some(O.none); + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when recipients are empty", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.some( + O.some({ + recipients: [] + }) + ) as pot.Pot, Error>; + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when recipients do not have a payment", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const internalPNMessagePot = pot.some( + O.some({ + recipients: [{}, {}, {}] + }) + ) as pot.Pot, Error>; + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + internalPNMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when user selected payments do not match", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + appState, + pnMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBeUndefined(); + }); + it("should return undefined when user selected payments do not match", () => { + const appState = appReducer(undefined, applicationChangeState("active")); + const rptId = `${p3CreditorTaxId}${p3NoticeCode}`; + const userSelectedPayments = + appState.entities.messages.payments.userSelectedPayments; + userSelectedPayments.add(rptId); + const finalState = { + ...appState, + entities: { + ...appState.entities, + messages: { + ...appState.entities.messages, + payments: { + ...appState.entities.messages.payments, + userSelectedPayments + } + } + } + } as GlobalState; + const pnUserSelectedPaymentRptId = pnUserSelectedPaymentRptIdSelector( + finalState, + pnMessagePot + ); + expect(pnUserSelectedPaymentRptId).toBe(rptId); + }); +}); diff --git a/ts/features/pn/store/reducers/__test__/payments.test.ts b/ts/features/pn/store/reducers/__test__/payments.test.ts new file mode 100644 index 00000000000..1643ee00c8c --- /dev/null +++ b/ts/features/pn/store/reducers/__test__/payments.test.ts @@ -0,0 +1,215 @@ +import { appReducer } from "../../../../../store/reducers"; +import { updatePaymentForMessage } from "../../../../messages/store/actions"; +import { UIMessageId } from "../../../../messages/types"; +import { paymentsButtonStateSelector } from "../payments"; +import { Detail_v2Enum } from "../../../../../../definitions/backend/PaymentProblemJson"; +import { reproduceSequence } from "../../../../../utils/tests"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { Action } from "../../../../../store/actions/types"; +import { NotificationPaymentInfo } from "../../../../../../definitions/pn/NotificationPaymentInfo"; + +describe("paymentsButtonStateSelector", () => { + it("should return hidden for an unmatching message Id on store", () => { + const updatePaymentForMessageAction = updatePaymentForMessage.request({ + messageId: "m1" as UIMessageId, + paymentId: "p1" + }); + const startingState = appReducer(undefined, updatePaymentForMessageAction); + const buttonState = paymentsButtonStateSelector( + startingState, + "m2" as UIMessageId, + undefined, + 5 + ); + expect(buttonState).toBe("hidden"); + }); + it("should return hidden when all visible payments are processed", () => { + const sequenceOfActions: ReadonlyArray = [ + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n1", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n2", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n3", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n4", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n5", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ]; + const appState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const payments = [ + { + noticeCode: "n1", + creditorTaxId: "c1" + }, + { + noticeCode: "n2", + creditorTaxId: "c1" + }, + { + noticeCode: "n3", + creditorTaxId: "c1" + }, + { + noticeCode: "n4", + creditorTaxId: "c1" + }, + { + noticeCode: "n5", + creditorTaxId: "c1" + } + ] as Array; + const buttonState = paymentsButtonStateSelector( + appState, + "m1" as UIMessageId, + payments, + 5 + ); + expect(buttonState).toBe("hidden"); + }); + it("should return visibleLoading when all visible payments are processing", () => { + const sequenceOfActions: ReadonlyArray = [ + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n6", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n7", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n8", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n9", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n10", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ]; + const appState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const payments = [ + { + noticeCode: "n1", + creditorTaxId: "c1" + }, + { + noticeCode: "n2", + creditorTaxId: "c1" + }, + { + noticeCode: "n3", + creditorTaxId: "c1" + }, + { + noticeCode: "n4", + creditorTaxId: "c1" + }, + { + noticeCode: "n5", + creditorTaxId: "c1" + } + ] as Array; + const buttonState = paymentsButtonStateSelector( + appState, + "m1" as UIMessageId, + payments, + 5 + ); + expect(buttonState).toBe("visibleLoading"); + }); + it("should return visibleEnabled when at least one visible payment has completed processing", () => { + const sequenceOfActions: ReadonlyArray = [ + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n5", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n7", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n8", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n9", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }), + updatePaymentForMessage.failure({ + messageId: "m1" as UIMessageId, + paymentId: "c1n10", + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ]; + const appState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const payments = [ + { + noticeCode: "n1", + creditorTaxId: "c1" + }, + { + noticeCode: "n2", + creditorTaxId: "c1" + }, + { + noticeCode: "n3", + creditorTaxId: "c1" + }, + { + noticeCode: "n4", + creditorTaxId: "c1" + }, + { + noticeCode: "n5", + creditorTaxId: "c1" + } + ] as Array; + const buttonState = paymentsButtonStateSelector( + appState, + "m1" as UIMessageId, + payments, + 5 + ); + expect(buttonState).toBe("visibleEnabled"); + }); +}); diff --git a/ts/features/pn/store/reducers/__tests__/payments.test.ts b/ts/features/pn/store/reducers/__tests__/payments.test.ts deleted file mode 100644 index 5fad349bfd9..00000000000 --- a/ts/features/pn/store/reducers/__tests__/payments.test.ts +++ /dev/null @@ -1,698 +0,0 @@ -import { Detail_v2Enum } from "../../../../../../definitions/backend/PaymentProblemJson"; -import { PaymentRequestsGetResponse } from "../../../../../../definitions/backend/PaymentRequestsGetResponse"; -import { NotificationPaymentInfo } from "../../../../../../definitions/pn/NotificationPaymentInfo"; -import { reloadAllMessages } from "../../../../messages/store/actions"; -import { Action } from "../../../../../store/actions/types"; -import { appReducer } from "../../../../../store/reducers"; -import { UIMessageId } from "../../../../messages/types"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { reproduceSequence } from "../../../../../utils/tests"; -import { - remoteError, - remoteLoading, - remoteReady, - remoteUndefined -} from "../../../../../common/model/RemoteValue"; -import { - clearSelectedPayment, - setSelectedPayment, - updatePaymentForMessage -} from "../../actions"; -import { - initialState, - paymentStatusForUISelector, - paymentsButtonStateSelector, - paymentsReducer, - selectedPaymentIdSelector, - shouldUpdatePaymentSelector -} from "../payments"; - -describe("PN Payments reducer's tests", () => { - it("Should match initial state upon initialization", () => { - const firstState = paymentsReducer(undefined, {} as Action); - expect(firstState).toEqual(initialState); - }); - it("Should have undefined value for an undefined Message Id", () => { - const requestAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const paymentsState = paymentsReducer(undefined, requestAction); - const unknownMessageId = "m2" as UIMessageId; - const messageState = paymentsState[unknownMessageId]; - expect(messageState).toBeUndefined(); - }); - it("Should have undefined value for an unknown paymentId", () => { - const messageId = "m1" as UIMessageId; - const requestAction = updatePaymentForMessage.request({ - messageId, - paymentId: "p1" - }); - const paymentsState = paymentsReducer(undefined, requestAction); - const messageState = paymentsState[messageId]; - expect(messageState).toBeTruthy(); - const unknownPaymentId = "p2"; - const paymentState = messageState?.[unknownPaymentId]; - expect(paymentState).toBeUndefined(); - }); - it("Should have remoteLoading value for a updatePaymentForMessage.request", () => { - const messageId = "m1" as UIMessageId; - const paymentId = "p1"; - const requestAction = updatePaymentForMessage.request({ - messageId, - paymentId - }); - const paymentsState = paymentsReducer(undefined, requestAction); - const messageState = paymentsState[messageId]; - expect(messageState).toBeTruthy(); - const paymentState = messageState?.[paymentId]; - expect(paymentState).toBe(remoteLoading); - }); - it("Should have remoteReady value for a updatePaymentForMessage.success", () => { - const messageId = "m1" as UIMessageId; - const paymentId = "p1"; - const requestAction = updatePaymentForMessage.request({ - messageId, - paymentId - }); - const paymentsState = paymentsReducer(undefined, requestAction); - const paymentData = { - importoSingoloVersamento: 100, - codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - } as PaymentRequestsGetResponse; - const successAction = updatePaymentForMessage.success({ - messageId, - paymentId, - paymentData - }); - const updatedPaymentsState = paymentsReducer(paymentsState, successAction); - const messageState = updatedPaymentsState[messageId]; - expect(messageState).toBeTruthy(); - const paymentState = messageState?.[paymentId]; - const remoteSuccessPaymentData = remoteReady(paymentData); - expect(paymentState).toStrictEqual(remoteSuccessPaymentData); - }); - it("Should have remoteError value for a updatePaymentForMessage.failure", () => { - const messageId = "m1" as UIMessageId; - const paymentId = "p1"; - const requestAction = updatePaymentForMessage.request({ - messageId, - paymentId - }); - const paymentsState = paymentsReducer(undefined, requestAction); - const details = Detail_v2Enum.CANALE_BUSTA_ERRATA; - const failureAction = updatePaymentForMessage.failure({ - messageId, - paymentId, - details - }); - const updatedPaymentsState = paymentsReducer(paymentsState, failureAction); - const messageState = updatedPaymentsState[messageId]; - expect(messageState).toBeTruthy(); - const paymentState = messageState?.[paymentId]; - const remoteSuccessPaymentData = remoteError(details); - expect(paymentState).toStrictEqual(remoteSuccessPaymentData); - }); - it("Should handle multiple payments for a single message", () => { - const messageId = "m1" as UIMessageId; - const paymentId1 = "p1"; - const requestAction = updatePaymentForMessage.request({ - messageId, - paymentId: paymentId1 - }); - const firstStateGeneration = paymentsReducer(undefined, requestAction); - const paymentId2 = "p2"; - const secondPaymentData = { - importoSingoloVersamento: 100, - codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - } as PaymentRequestsGetResponse; - const successAction = updatePaymentForMessage.success({ - messageId, - paymentId: paymentId2, - paymentData: secondPaymentData - }); - const secondStateGeneration = paymentsReducer( - firstStateGeneration, - successAction - ); - const paymentId3 = "p3"; - const thirdPaymentDetails = Detail_v2Enum.CANALE_BUSTA_ERRATA; - const failureAction = updatePaymentForMessage.failure({ - messageId, - paymentId: paymentId3, - details: thirdPaymentDetails - }); - const finalStateGeneration = paymentsReducer( - secondStateGeneration, - failureAction - ); - const messageState = finalStateGeneration[messageId]; - expect(messageState).toBeTruthy(); - const firstPaymentState = messageState?.[paymentId1]; - expect(firstPaymentState).toBe(remoteLoading); - const secondPaymentState = messageState?.[paymentId2]; - expect(secondPaymentState).toStrictEqual(remoteReady(secondPaymentData)); - const thirdPaymentState = messageState?.[paymentId3]; - expect(thirdPaymentState).toStrictEqual(remoteError(thirdPaymentDetails)); - }); - it("Should handle multiple payments for multiple messages", () => { - const messageId1 = "m1" as UIMessageId; - const paymentId1 = "p1"; - const requestAction = updatePaymentForMessage.request({ - messageId: messageId1, - paymentId: paymentId1 - }); - const firstStateGeneration = paymentsReducer(undefined, requestAction); - const messageId2 = "m2" as UIMessageId; - const successfulPaymentData = { - importoSingoloVersamento: 100, - codiceContestoPagamento: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - } as PaymentRequestsGetResponse; - const successAction = updatePaymentForMessage.success({ - messageId: messageId2, - paymentId: paymentId1, - paymentData: successfulPaymentData - }); - const secondStateGeneration = paymentsReducer( - firstStateGeneration, - successAction - ); - const messageId3 = "m3" as UIMessageId; - const failedPaymentDetails = Detail_v2Enum.CANALE_BUSTA_ERRATA; - const failureAction = updatePaymentForMessage.failure({ - messageId: messageId3, - paymentId: paymentId1, - details: failedPaymentDetails - }); - const finalStateGeneration = paymentsReducer( - secondStateGeneration, - failureAction - ); - const message1State = finalStateGeneration[messageId1]; - expect(message1State).toBeTruthy(); - const firstPaymentState = message1State?.[paymentId1]; - expect(firstPaymentState).toBe(remoteLoading); - const message2State = finalStateGeneration[messageId2]; - expect(message2State).toBeTruthy(); - const secondPaymentState = message2State?.[paymentId1]; - expect(secondPaymentState).toStrictEqual( - remoteReady(successfulPaymentData) - ); - const message3State = finalStateGeneration[messageId3]; - expect(message3State).toBeTruthy(); - const thirdPaymentState = message3State?.[paymentId1]; - expect(thirdPaymentState).toStrictEqual(remoteError(failedPaymentDetails)); - }); - it("Should remove payment statuses on updatePaymentForMessage.cancel", () => { - const messageId1 = "m1" as UIMessageId; - const paymentId1 = "p1"; - const requestAction1 = updatePaymentForMessage.request({ - messageId: messageId1, - paymentId: paymentId1 - }); - const firstStateGeneration = paymentsReducer(undefined, requestAction1); - const messageId2 = "m2" as UIMessageId; - const requestAction2 = updatePaymentForMessage.request({ - messageId: messageId2, - paymentId: paymentId1 - }); - const secondStateGeneration = paymentsReducer( - firstStateGeneration, - requestAction2 - ); - const paymentId2 = "p2"; - const requestAction3 = updatePaymentForMessage.request({ - messageId: messageId2, - paymentId: paymentId2 - }); - const thirdStateGeneration = paymentsReducer( - secondStateGeneration, - requestAction3 - ); - const messageId3 = "m3" as UIMessageId; - const requestAction4 = updatePaymentForMessage.request({ - messageId: messageId3, - paymentId: paymentId1 - }); - const fourthStateGeneration = paymentsReducer( - thirdStateGeneration, - requestAction4 - ); - const requestAction5 = updatePaymentForMessage.request({ - messageId: messageId3, - paymentId: paymentId2 - }); - const fifthStateGeneration = paymentsReducer( - fourthStateGeneration, - requestAction5 - ); - const paymentId3 = "p3"; - const requestAction6 = updatePaymentForMessage.request({ - messageId: messageId3, - paymentId: paymentId3 - }); - const sixthStateGeneration = paymentsReducer( - fifthStateGeneration, - requestAction6 - ); - - const m1S1 = sixthStateGeneration[messageId1]; - expect(m1S1).toBeTruthy(); - const m1p1S1 = m1S1?.[paymentId1]; - expect(m1p1S1).toStrictEqual(remoteLoading); - - const m2S1 = sixthStateGeneration[messageId2]; - expect(m2S1).toBeTruthy(); - const m2p1S1 = m2S1?.[paymentId1]; - expect(m2p1S1).toStrictEqual(remoteLoading); - const m2p2S1 = m2S1?.[paymentId2]; - expect(m2p2S1).toStrictEqual(remoteLoading); - - const m3S1 = sixthStateGeneration[messageId3]; - expect(m3S1).toBeTruthy(); - const m3p1S1 = m3S1?.[paymentId1]; - expect(m3p1S1).toStrictEqual(remoteLoading); - const m3p2S1 = m3S1?.[paymentId2]; - expect(m3p2S1).toStrictEqual(remoteLoading); - const m3p3S1 = m3S1?.[paymentId3]; - expect(m3p3S1).toStrictEqual(remoteLoading); - - const cancelPaymentAction = updatePaymentForMessage.cancel([ - { - messageId: messageId1, - paymentId: paymentId1 - }, - { - messageId: messageId2, - paymentId: paymentId2 - }, - { - messageId: messageId3, - paymentId: paymentId2 - }, - { - messageId: messageId3, - paymentId: paymentId3 - } - ]); - const finalStateGeneration = paymentsReducer( - sixthStateGeneration, - cancelPaymentAction - ); - - const m1S2 = finalStateGeneration[messageId1]; - expect(m1S2).toBeTruthy(); - const m1p1S2 = m1S2?.[paymentId1]; - expect(m1p1S2).toBeUndefined(); - - const m2S2 = finalStateGeneration[messageId2]; - expect(m2S2).toBeTruthy(); - const m2p1S2 = m2S2?.[paymentId1]; - expect(m2p1S2).toStrictEqual(remoteLoading); - const m2p2S2 = m2S2?.[paymentId2]; - expect(m2p2S2).toBeUndefined(); - - const m3S2 = finalStateGeneration[messageId3]; - expect(m3S2).toBeTruthy(); - const m3p1S2 = m3S2?.[paymentId1]; - expect(m3p1S2).toStrictEqual(remoteLoading); - const m3p2S2 = m3S2?.[paymentId2]; - expect(m3p2S2).toBeUndefined(); - const m3p3S2 = m3S2?.[paymentId3]; - expect(m3p3S2).toBeUndefined(); - }); - it("Should have the paymentId for a setSelectedPayment action", () => { - const paymentId = "p1"; - const setSelectedPaymentAction = setSelectedPayment(paymentId); - const paymentsState = paymentsReducer(undefined, setSelectedPaymentAction); - const selectedPaymentId = paymentsState.selectedPayment; - expect(selectedPaymentId).toBe(paymentId); - }); - it("Should clear the paymentId for a clearSelectedPayment action", () => { - const paymentId = "p1"; - const setSelectedPaymentAction = setSelectedPayment(paymentId); - const startingPaymentsState = paymentsReducer( - undefined, - setSelectedPaymentAction - ); - const startingSelectedPaymentId = startingPaymentsState.selectedPayment; - expect(startingSelectedPaymentId).toBe(paymentId); - const endingPaymentsState = paymentsReducer( - startingPaymentsState, - clearSelectedPayment() - ); - const endingSelectedPaymentId = endingPaymentsState.selectedPayment; - expect(endingSelectedPaymentId).toBeUndefined(); - }); - it("Should clear the paymentId for a reloadAllMessages action", () => { - const paymentId = "p1"; - const setSelectedPaymentAction = setSelectedPayment(paymentId); - const startingPaymentsState = paymentsReducer( - undefined, - setSelectedPaymentAction - ); - const startingSelectedPaymentId = startingPaymentsState.selectedPayment; - expect(startingSelectedPaymentId).toBe(paymentId); - const endingPaymentsState = paymentsReducer( - startingPaymentsState, - reloadAllMessages.request({ pageSize: 12, filter: {} }) - ); - const endingSelectedPaymentId = endingPaymentsState.selectedPayment; - expect(endingSelectedPaymentId).toBeUndefined(); - }); -}); - -describe("PN Payments selectors' tests", () => { - it("shouldUpdatePaymentSelector should return true for an unmatching message Id", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const shouldUpdatePayment = shouldUpdatePaymentSelector( - state, - "m2" as UIMessageId, - "p1" - ); - expect(shouldUpdatePayment).toBeTruthy(); - }); - it("shouldUpdatePaymentSelector should return true for a matching message Id with an unmatching payment Id", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const shouldUpdatePayment = shouldUpdatePaymentSelector( - state, - "m1" as UIMessageId, - "p2" - ); - expect(shouldUpdatePayment).toBeTruthy(); - }); - it("shouldUpdatePaymentSelector should return false for a matching pair", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const shouldUpdatePayment = shouldUpdatePaymentSelector( - state, - "m1" as UIMessageId, - "p1" - ); - expect(shouldUpdatePayment).toBeFalsy(); - }); - it("paymentStatusForUISelector should return remoteUndefined for an unmatching message Id", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const paymentStatus = paymentStatusForUISelector( - state, - "m2" as UIMessageId, - "p1" - ); - expect(paymentStatus).toBe(remoteUndefined); - }); - it("paymentStatusForUISelector should return remoteUndefined for a matching message Id with an unmatching payment Id", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const paymentStatus = paymentStatusForUISelector( - state, - "m1" as UIMessageId, - "p2" - ); - expect(paymentStatus).toBe(remoteUndefined); - }); - it("paymentStatusForUISelector should return remoteUndefined for a matching that is loading", () => { - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const paymentStatusOnStore = - state.features.pn.payments["m1" as UIMessageId]?.p1; - expect(paymentStatusOnStore).toBe(remoteLoading); - const paymentStatus = paymentStatusForUISelector( - state, - "m1" as UIMessageId, - "p1" - ); - expect(paymentStatus).toBe(remoteUndefined); - }); - it("paymentStatusForUISelector should return remoteReady for a matching that is payable", () => { - const paymentData = {} as PaymentRequestsGetResponse; - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.success({ - messageId: "m1" as UIMessageId, - paymentId: "p1", - paymentData - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const paymentStatus = paymentStatusForUISelector( - state, - "m1" as UIMessageId, - "p1" - ); - expect(paymentStatus).toStrictEqual(remoteReady(paymentData)); - }); - it("paymentStatusForUISelector should return remoteError for a matching that is not payable anymore", () => { - const details = Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO; - const startingState = appReducer(undefined, {} as Action); - const updatePaymentForMessageAction = updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "p1", - details - }); - const state = appReducer(startingState, updatePaymentForMessageAction); - const paymentStatus = paymentStatusForUISelector( - state, - "m1" as UIMessageId, - "p1" - ); - expect(paymentStatus).toStrictEqual(remoteError(details)); - }); - it("paymentsButtonStateSelector should return hidden for an unmatching message Id on store", () => { - const updatePaymentForMessageAction = updatePaymentForMessage.request({ - messageId: "m1" as UIMessageId, - paymentId: "p1" - }); - const startingState = appReducer(undefined, updatePaymentForMessageAction); - const buttonState = paymentsButtonStateSelector( - startingState, - "m2" as UIMessageId, - undefined, - 5 - ); - expect(buttonState).toBe("hidden"); - }); - it("paymentsButtonStateSelector should return hidden when all visible payments are processed", () => { - const sequenceOfActions: ReadonlyArray = [ - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n1", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n2", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n3", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n4", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n5", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }) - ]; - const appState = reproduceSequence( - {} as GlobalState, - appReducer, - sequenceOfActions - ); - const payments = [ - { - noticeCode: "n1", - creditorTaxId: "c1" - }, - { - noticeCode: "n2", - creditorTaxId: "c1" - }, - { - noticeCode: "n3", - creditorTaxId: "c1" - }, - { - noticeCode: "n4", - creditorTaxId: "c1" - }, - { - noticeCode: "n5", - creditorTaxId: "c1" - } - ] as Array; - const buttonState = paymentsButtonStateSelector( - appState, - "m1" as UIMessageId, - payments, - 5 - ); - expect(buttonState).toBe("hidden"); - }); - it("paymentsButtonStateSelector should return visibleLoading when all visible payments are processing", () => { - const sequenceOfActions: ReadonlyArray = [ - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n6", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n7", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n8", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n9", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n10", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }) - ]; - const appState = reproduceSequence( - {} as GlobalState, - appReducer, - sequenceOfActions - ); - const payments = [ - { - noticeCode: "n1", - creditorTaxId: "c1" - }, - { - noticeCode: "n2", - creditorTaxId: "c1" - }, - { - noticeCode: "n3", - creditorTaxId: "c1" - }, - { - noticeCode: "n4", - creditorTaxId: "c1" - }, - { - noticeCode: "n5", - creditorTaxId: "c1" - } - ] as Array; - const buttonState = paymentsButtonStateSelector( - appState, - "m1" as UIMessageId, - payments, - 5 - ); - expect(buttonState).toBe("visibleLoading"); - }); - it("paymentsButtonStateSelector should return visibleEnabled when at least one visible payment has completed processing", () => { - const sequenceOfActions: ReadonlyArray = [ - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n5", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n7", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n8", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n9", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }), - updatePaymentForMessage.failure({ - messageId: "m1" as UIMessageId, - paymentId: "c1n10", - details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO - }) - ]; - const appState = reproduceSequence( - {} as GlobalState, - appReducer, - sequenceOfActions - ); - const payments = [ - { - noticeCode: "n1", - creditorTaxId: "c1" - }, - { - noticeCode: "n2", - creditorTaxId: "c1" - }, - { - noticeCode: "n3", - creditorTaxId: "c1" - }, - { - noticeCode: "n4", - creditorTaxId: "c1" - }, - { - noticeCode: "n5", - creditorTaxId: "c1" - } - ] as Array; - const buttonState = paymentsButtonStateSelector( - appState, - "m1" as UIMessageId, - payments, - 5 - ); - expect(buttonState).toBe("visibleEnabled"); - }); - it("selectedPaymentIdSelector should return undefined when none is set", () => { - const appState = appReducer(undefined, {} as Action); - const selectedPaymentId = selectedPaymentIdSelector(appState); - expect(selectedPaymentId).toBeUndefined(); - }); - it("selectedPaymentIdSelector should return the selected payment", () => { - const appState = appReducer(undefined, setSelectedPayment("p1")); - const selectedPaymentId = selectedPaymentIdSelector(appState); - expect(selectedPaymentId).toBe("p1"); - }); -}); diff --git a/ts/features/pn/store/reducers/activation.ts b/ts/features/pn/store/reducers/activation.ts index 62c3333a155..d172c68b4c6 100644 --- a/ts/features/pn/store/reducers/activation.ts +++ b/ts/features/pn/store/reducers/activation.ts @@ -1,7 +1,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; import { getType } from "typesafe-actions"; -import { loadServicePreference } from "../../../../store/actions/services/servicePreference"; +import { loadServicePreference } from "../../../services/store/actions"; import { Action } from "../../../../store/actions/types"; import { GlobalState } from "../../../../store/reducers/types"; import { pnActivationUpsert } from "../actions"; diff --git a/ts/features/pn/store/reducers/index.ts b/ts/features/pn/store/reducers/index.ts index fb602d51ee6..59fc3d3f97f 100644 --- a/ts/features/pn/store/reducers/index.ts +++ b/ts/features/pn/store/reducers/index.ts @@ -1,24 +1,25 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import * as RA from "fp-ts/lib/ReadonlyArray"; import { combineReducers } from "redux"; import { createSelector } from "reselect"; import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import { Action } from "../../../../store/actions/types"; import { thirdPartyFromIdSelector } from "../../../messages/store/reducers/thirdPartyById"; import { toPNMessage } from "../types/transformers"; -import { UIAttachmentId, UIMessageId } from "../../../messages/types"; +import { UIMessageId } from "../../../messages/types"; import { GlobalState } from "../../../../store/reducers/types"; +import { PNMessage } from "../types/types"; +import { getRptIdStringFromPayment } from "../../utils/rptId"; +import { isUserSelectedPaymentSelector } from "../../../messages/store/reducers/payments"; import { pnActivationReducer, PnActivationState } from "./activation"; -import { MultiplePaymentState, paymentsReducer } from "./payments"; export type PnState = { activation: PnActivationState; - payments: MultiplePaymentState; }; export const pnReducer = combineReducers({ - activation: pnActivationReducer, - payments: paymentsReducer + activation: pnActivationReducer }); export const pnMessageFromIdSelector = createSelector( @@ -29,7 +30,7 @@ export const pnMessageFromIdSelector = createSelector( export const pnMessageAttachmentSelector = (state: GlobalState) => (ioMessageId: UIMessageId) => - (pnMessageAttachmentId: UIAttachmentId) => + (pnMessageAttachmentId: string) => pipe( pnMessageFromIdSelector(state, ioMessageId), pot.toOption, @@ -42,3 +43,32 @@ export const pnMessageAttachmentSelector = ) ) ); + +export const pnUserSelectedPaymentRptIdSelector = ( + state: GlobalState, + pnMessagePot: pot.Pot, Error> +) => + pipe( + pnMessagePot, + pot.toOption, + O.flatten, + O.map(message => message.recipients), + O.chain(recipients => + pipe( + recipients, + RA.findFirstMap(recipient => + pipe( + recipient.payment, + O.fromNullable, + O.map(getRptIdStringFromPayment), + O.map(rptId => isUserSelectedPaymentSelector(state, rptId)), + O.getOrElse(() => false) + ) + ? O.fromNullable(recipient.payment) + : O.none + ) + ) + ), + O.map(getRptIdStringFromPayment), + O.toUndefined + ); diff --git a/ts/features/pn/store/reducers/payments.ts b/ts/features/pn/store/reducers/payments.ts index 86a73f167cd..cb941c1006c 100644 --- a/ts/features/pn/store/reducers/payments.ts +++ b/ts/features/pn/store/reducers/payments.ts @@ -2,130 +2,18 @@ import { pipe } from "fp-ts/lib/function"; import * as B from "fp-ts/lib/boolean"; import * as RA from "fp-ts/lib/ReadonlyArray"; import * as O from "fp-ts/lib/Option"; -import { getType } from "typesafe-actions"; -import { Action } from "../../../../store/actions/types"; import { UIMessageId } from "../../../messages/types"; import { GlobalState } from "../../../../store/reducers/types"; import { isError, - isLoading, isReady, - isUndefined, - remoteError, - remoteLoading, - remoteReady, - remoteUndefined, RemoteValue } from "../../../../common/model/RemoteValue"; -import { - clearSelectedPayment, - setSelectedPayment, - updatePaymentForMessage -} from "../actions"; import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; import { NotificationPaymentInfo } from "../../../../../definitions/pn/NotificationPaymentInfo"; -import { getRptIdStringFromPayment } from "../../utils/rptId"; -import { reloadAllMessages } from "../../../messages/store/actions"; - -export type MultiplePaymentState = { - [key: UIMessageId]: SinglePaymentState | undefined; - selectedPayment?: string; -}; - -export type SinglePaymentState = { - [key: string]: - | RemoteValue - | undefined; -}; - -export const initialState: MultiplePaymentState = {}; - -export const paymentsReducer = ( - state: MultiplePaymentState = initialState, - action: Action -): MultiplePaymentState => { - switch (action.type) { - case getType(updatePaymentForMessage.request): - return { - ...state, - [action.payload.messageId]: { - ...state[action.payload.messageId], - [action.payload.paymentId]: remoteLoading - } - }; - case getType(updatePaymentForMessage.success): - return { - ...state, - [action.payload.messageId]: { - ...state[action.payload.messageId], - [action.payload.paymentId]: remoteReady(action.payload.paymentData) - } - }; - case getType(updatePaymentForMessage.failure): - return { - ...state, - [action.payload.messageId]: { - ...state[action.payload.messageId], - [action.payload.paymentId]: remoteError(action.payload.details) - } - }; - case getType(updatePaymentForMessage.cancel): - return action.payload.reduce( - (previousState, queuedUpdateActionPayload) => ({ - ...previousState, - [queuedUpdateActionPayload.messageId]: { - ...previousState[queuedUpdateActionPayload.messageId], - [queuedUpdateActionPayload.paymentId]: undefined - } - }), - state - ); - case getType(setSelectedPayment): - return { - ...state, - selectedPayment: action.payload.paymentId - }; - case getType(clearSelectedPayment): - return { - ...state, - selectedPayment: undefined - }; - case getType(reloadAllMessages.request): - return initialState; - } - return state; -}; - -export const shouldUpdatePaymentSelector = ( - state: GlobalState, - messageId: UIMessageId, - paymentId: string -) => - pipe( - state.features.pn.payments[messageId], - O.fromNullable, - O.chainNullableK(multiplePaymentState => multiplePaymentState[paymentId]), - O.getOrElse>( - () => remoteUndefined - ), - isUndefined - ); - -export const paymentStatusForUISelector = ( - state: GlobalState, - messageId: UIMessageId, - paymentId: string -): RemoteValue => - pipe( - state.features.pn.payments[messageId], - O.fromNullable, - O.chainNullableK(multiplePaymentState => multiplePaymentState[paymentId]), - O.getOrElse>( - () => remoteUndefined - ), - remoteValue => (isLoading(remoteValue) ? remoteUndefined : remoteValue) - ); +import { getRptIdStringFromPayment } from "../../../pn/utils/rptId"; +import { SinglePaymentState } from "../../../messages/store/reducers/payments"; export const paymentsButtonStateSelector = ( state: GlobalState, @@ -134,7 +22,7 @@ export const paymentsButtonStateSelector = ( maxVisiblePaymentCount: number ) => pipe( - state.features.pn.payments[messageId], + state.entities.messages.payments[messageId], O.fromNullable, computeUpdatedPaymentCount(payments, maxVisiblePaymentCount), buttonStateFromUpdatedPaymentCount(payments, maxVisiblePaymentCount) @@ -236,6 +124,3 @@ const buttonStateFromUpdatedPaymentCount = ) ) ); - -export const selectedPaymentIdSelector = (state: GlobalState) => - state.features.pn.payments.selectedPayment; diff --git a/ts/features/pn/store/sagas/watchPaymentStatusSaga.ts b/ts/features/pn/store/sagas/watchPaymentStatusSaga.ts index 81ad66e9d0d..9fef0d163c9 100644 --- a/ts/features/pn/store/sagas/watchPaymentStatusSaga.ts +++ b/ts/features/pn/store/sagas/watchPaymentStatusSaga.ts @@ -2,10 +2,10 @@ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { call, race, select, take } from "typed-redux-saga/macro"; import { ActionType, isActionOf } from "typesafe-actions"; +import { updatePaymentForMessage } from "../../../messages/store/actions"; import { cancelPaymentStatusTracking, - startPaymentStatusTracking, - updatePaymentForMessage + startPaymentStatusTracking } from "../actions"; import { maxVisiblePaymentCountGenerator, diff --git a/ts/features/pn/store/sagas/watchPnSaga.ts b/ts/features/pn/store/sagas/watchPnSaga.ts index 4e79cf929be..b4bafab9ad4 100644 --- a/ts/features/pn/store/sagas/watchPnSaga.ts +++ b/ts/features/pn/store/sagas/watchPnSaga.ts @@ -4,7 +4,7 @@ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import * as E from "fp-ts/lib/Either"; import { SagaIterator } from "redux-saga"; -import { call, fork, put, select, takeLatest } from "typed-redux-saga/macro"; +import { call, put, select, takeLatest } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; import { apiUrlPrefix } from "../../../../config"; import { isPnTestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; @@ -16,10 +16,8 @@ import { trackPNServiceStatusChangeError, trackPNServiceStatusChangeSuccess } from "../../analytics"; -import { servicePreferenceSelector } from "../../../../store/reducers/entities/services/servicePreference"; -import { isServicePreferenceResponseSuccess } from "../../../../types/services/ServicePreferenceResponse"; -import { BackendClient } from "../../../../api/backend"; -import { watchPaymentUpdateRequests } from "./watchPaymentUpdateRequests"; +import { servicePreferenceSelector } from "../../../services/store/reducers/servicePreference"; +import { isServicePreferenceResponseSuccess } from "../../../services/types/ServicePreferenceResponse"; import { watchPaymentStatusForMixpanelTracking } from "./watchPaymentStatusSaga"; function* handlePnActivation( @@ -75,10 +73,7 @@ function* reportPNServiceStatusOnFailure(predictedValue: boolean) { trackPNServiceStatusChangeError(isServiceActive); } -export function* watchPnSaga( - bearerToken: SessionToken, - getVerificaRpt: BackendClient["getVerificaRpt"] -): SagaIterator { +export function* watchPnSaga(bearerToken: SessionToken): SagaIterator { const pnClient = createPnClient(apiUrlPrefix, bearerToken); yield* takeLatest( @@ -87,8 +82,6 @@ export function* watchPnSaga( pnClient.upsertPNActivation ); - yield* fork(watchPaymentUpdateRequests, getVerificaRpt); - yield* takeLatest( startPaymentStatusTracking, watchPaymentStatusForMixpanelTracking diff --git a/ts/features/pn/store/types/transformers.ts b/ts/features/pn/store/types/transformers.ts index 3b0c5269074..c9beacf92aa 100644 --- a/ts/features/pn/store/types/transformers.ts +++ b/ts/features/pn/store/types/transformers.ts @@ -2,7 +2,6 @@ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; import { ThirdPartyMessage } from "../../../../../definitions/pn/ThirdPartyMessage"; -import { attachmentsFromThirdPartyMessage } from "../../../messages/store/reducers/transformers"; import { PNMessage } from "./types"; export const toPNMessage = ( @@ -16,9 +15,6 @@ export const toPNMessage = ( O.map(details => ({ ...details, created_at: messageFromApi.created_at, - attachments: pipe( - attachmentsFromThirdPartyMessage(messageFromApi), - O.toUndefined - ) + attachments: messageFromApi.third_party_message.attachments })) ); diff --git a/ts/features/pn/store/types/types.ts b/ts/features/pn/store/types/types.ts index 192e7d9652a..f8151d19877 100644 --- a/ts/features/pn/store/types/types.ts +++ b/ts/features/pn/store/types/types.ts @@ -1,7 +1,7 @@ +import { ThirdPartyAttachment } from "../../../../../definitions/backend/ThirdPartyAttachment"; import { IOReceivedNotification } from "../../../../../definitions/pn/IOReceivedNotification"; -import { UIAttachment } from "../../../messages/types"; export type PNMessage = IOReceivedNotification & { created_at: Date; - attachments?: ReadonlyArray; + attachments?: ReadonlyArray; }; diff --git a/ts/features/pn/utils/__tests__/index.test.ts b/ts/features/pn/utils/__tests__/index.test.ts index f16d3b7a63a..9877ffdf70e 100644 --- a/ts/features/pn/utils/__tests__/index.test.ts +++ b/ts/features/pn/utils/__tests__/index.test.ts @@ -1,18 +1,13 @@ import * as O from "fp-ts/lib/Option"; import { isPNOptInMessage } from ".."; -import { UIService } from "../../../../store/reducers/entities/services/types"; import { GlobalState } from "../../../../store/reducers/types"; import { CTAS } from "../../../messages/types/MessageCTA"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -const pnOptInServiceId = () => "optInServiceId"; const navigateToServiceLink = () => "ioit://services/service-detail?serviceId=optInServiceId&activate=true"; -const getMockService = () => - ({ - id: pnOptInServiceId() - } as UIService); +const getMockPnOptInServiceId = () => "optInServiceId" as ServiceId; const getMockState = () => ({ @@ -20,7 +15,7 @@ const getMockState = () => status: O.some({ config: { pn: { - optInServiceId: pnOptInServiceId() + optInServiceId: getMockPnOptInServiceId() } } }) @@ -39,19 +34,17 @@ const getMockCTAs = () => } } as CTAS); -const getMaybeCTAs = () => O.some(getMockCTAs()); - type IsPNOptInMessageTestInputType = { testDescription: string; input: { - CTAs: O.Option; - service: UIService | undefined; + CTAs: CTAS | undefined; + serviceId: ServiceId | undefined; state: GlobalState; }; output: { isPNOptInMessage: boolean; - cta1HasServiceNavigationLink: boolean; - cta2HasServiceNavigationLink: boolean; + cta1LinksToPNService: boolean; + cta2LinksToPNService: boolean; }; }; @@ -59,97 +52,97 @@ const isPNOptInMessageTestInput: Array = [ { testDescription: "should detect the OptIn format and both CTAs", input: { - CTAs: getMaybeCTAs(), - service: getMockService(), + CTAs: getMockCTAs(), + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: true + cta1LinksToPNService: true, + cta2LinksToPNService: true } }, { testDescription: "should detect the OptIn format, the first CTA but not the second (when its input is undefined)", input: { - CTAs: O.some({ + CTAs: { ...getMockCTAs(), cta_2: undefined - }), - service: getMockService(), + }, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: true, + cta2LinksToPNService: false } }, { testDescription: "should detect the OptIn format, the first CTA but not the second (when its action does not contain a service navigation link)", input: { - CTAs: O.some({ + CTAs: { ...getMockCTAs(), cta_2: { text: "Attiva il servizio", action: "ioit://main/messages" } - }), - service: getMockService(), + }, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: true, + cta2LinksToPNService: false } }, { testDescription: "should detect the OptIn format, the second CTA but not the fist (when its action does not contain a service navigation link)", input: { - CTAs: O.some({ + CTAs: { ...getMockCTAs(), cta_1: { text: "Attiva il servizio", action: "ioit://main/messages" } - }), - service: getMockService(), + }, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: true + cta1LinksToPNService: false, + cta2LinksToPNService: true } }, { testDescription: "should not recognize the OptIn format, when the first CTA does not contain a service navigation link and the second is undefined", input: { - CTAs: O.some({ + CTAs: { cta_1: { text: "Attiva il servizio", action: "ioit://main/messages" } - }), - service: getMockService(), + }, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: "should not recognize the OptIn format, when both CTAs do not contain a service navigation link", input: { - CTAs: O.some({ + CTAs: { cta_1: { text: "Attiva il servizio", action: "ioit://main/messages" @@ -158,67 +151,64 @@ const isPNOptInMessageTestInput: Array = [ text: "Attiva il servizio", action: "ioit://main/services" } - }), - service: getMockService(), + }, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: - "should not recognize the OptIn format, when CTAs are O.none", + "should not recognize the OptIn format, when CTAs are not defined", input: { - CTAs: O.none, - service: getMockService(), + CTAs: undefined, + serviceId: getMockPnOptInServiceId(), state: getMockState() }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: "should not recognize the OptIn format, when the service's id is not the OptIn one", input: { - CTAs: getMaybeCTAs(), - service: { - ...getMockService(), - id: "NotTheOptInOne" as ServiceId - }, + CTAs: getMockCTAs(), + serviceId: "NotTheOptInOne" as ServiceId, state: getMockState() }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: "should not recognize the OptIn format, when the service is undefined", input: { - CTAs: getMaybeCTAs(), - service: undefined, + CTAs: getMockCTAs(), + serviceId: undefined, state: getMockState() }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: "should not recognize the OptIn format, when state.backendStatus.status.config.pn.optInServiceId does not match service.id", input: { - CTAs: getMaybeCTAs(), - service: getMockService(), + CTAs: getMockCTAs(), + serviceId: getMockPnOptInServiceId(), state: { ...getMockState(), backendStatus: { @@ -234,16 +224,16 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { testDescription: "should not recognize the OptIn format, when state.backendStatus.status is O.none", input: { - CTAs: getMaybeCTAs(), - service: getMockService(), + CTAs: getMockCTAs(), + serviceId: getMockPnOptInServiceId(), state: { ...getMockState(), backendStatus: { @@ -253,8 +243,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } } ]; @@ -264,7 +254,7 @@ describe("isPNOptInMessage", () => { it(testData.testDescription, () => { const isPNOptInMessageInfo = isPNOptInMessage( testData.input.CTAs, - testData.input.service, + testData.input.serviceId, testData.input.state ); expect(isPNOptInMessageInfo).toEqual(testData.output); diff --git a/ts/features/pn/utils/index.ts b/ts/features/pn/utils/index.ts index a7d4ef91778..48fea153997 100644 --- a/ts/features/pn/utils/index.ts +++ b/ts/features/pn/utils/index.ts @@ -1,12 +1,8 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { identity, pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import * as E from "fp-ts/lib/Either"; import * as RA from "fp-ts/lib/ReadonlyArray"; -import { RptIdFromString } from "@pagopa/io-pagopa-commons/lib/pagopa"; -import { Dispatch } from "redux"; import I18n from "../../../i18n"; -import { UIService } from "../../../store/reducers/entities/services/types"; import { PNMessage } from "../store/types/types"; import { NotificationStatus } from "../../../../definitions/pn/NotificationStatus"; import { CTAS } from "../../messages/types/MessageCTA"; @@ -14,13 +10,9 @@ import { isServiceDetailNavigationLink } from "../../../utils/internalLink"; import { GlobalState } from "../../../store/reducers/types"; import { NotificationRecipient } from "../../../../definitions/pn/NotificationRecipient"; import { NotificationPaymentInfo } from "../../../../definitions/pn/NotificationPaymentInfo"; -import { paymentInitializeState } from "../../../store/actions/wallet/payment"; -import NavigationService from "../../../navigation/NavigationService"; -import ROUTES from "../../../navigation/routes"; -import { setSelectedPayment } from "../store/actions"; -import { trackPNPaymentStart } from "../analytics"; import { ATTACHMENT_CATEGORY } from "../../messages/types/attachmentCategory"; -import { UIAttachment } from "../../messages/types"; +import { ThirdPartyAttachment } from "../../../../definitions/backend/ThirdPartyAttachment"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; export const maxVisiblePaymentCountGenerator = () => 5; @@ -32,56 +24,51 @@ export function getNotificationStatusInfo(status: NotificationStatus) { export type PNOptInMessageInfo = { isPNOptInMessage: boolean; - cta1HasServiceNavigationLink: boolean; - cta2HasServiceNavigationLink: boolean; + cta1LinksToPNService: boolean; + cta2LinksToPNService: boolean; }; export const isPNOptInMessage = ( - maybeCtas: O.Option, - service: UIService | undefined, + ctas: CTAS | undefined, + serviceId: ServiceId | undefined, state: GlobalState ) => pipe( - service, + serviceId, O.fromNullable, - O.chain(service => + O.chain(serviceId => pipe( state.backendStatus.status, O.map( - backendStatus => backendStatus.config.pn.optInServiceId === service.id + backendStatus => backendStatus.config.pn.optInServiceId === serviceId ) ) ), O.filter(identity), - O.chain(_ => + O.chain(() => pipe( - maybeCtas, + ctas, + O.fromNullable, O.map(ctas => ({ - cta1HasServiceNavigationLink: isServiceDetailNavigationLink( + cta1LinksToPNService: isServiceDetailNavigationLink( ctas.cta_1.action ), - cta2HasServiceNavigationLink: + cta2LinksToPNService: !!ctas.cta_2 && isServiceDetailNavigationLink(ctas.cta_2.action) })), - O.map( - ctaNavigationLinkInfo => - ({ - isPNOptInMessage: - ctaNavigationLinkInfo.cta1HasServiceNavigationLink || - ctaNavigationLinkInfo.cta2HasServiceNavigationLink, - ...ctaNavigationLinkInfo - } as PNOptInMessageInfo) - ) + O.map(ctaNavigationLinkInfo => ({ + isPNOptInMessage: + ctaNavigationLinkInfo.cta1LinksToPNService || + ctaNavigationLinkInfo.cta2LinksToPNService, + ...ctaNavigationLinkInfo + })) ) ), - O.getOrElse( - () => - ({ - isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false - } as PNOptInMessageInfo) - ) + O.getOrElse(() => ({ + isPNOptInMessage: false, + cta1LinksToPNService: false, + cta2LinksToPNService: false + })) ); export const paymentsFromPNMessagePot = ( @@ -125,31 +112,6 @@ export const containsF24FromPNMessagePot = ( pipe( pot.getOrElse(potMessage, O.none), O.chainNullableK(message => message.attachments), - O.getOrElse>(() => []), + O.getOrElse>(() => []), RA.some(attachment => attachment.category === ATTACHMENT_CATEGORY.F24) ); - -export const initializeAndNavigateToWalletForPayment = ( - paymentId: string, - dispatch: Dispatch, - decodeErrorCallback: (() => void) | undefined, - preNavigationCallback: (() => void) | undefined = undefined -) => { - const eitherRptId = RptIdFromString.decode(paymentId); - if (E.isLeft(eitherRptId)) { - decodeErrorCallback?.(); - return; - } - - preNavigationCallback?.(); - - trackPNPaymentStart(); - - dispatch(setSelectedPayment(paymentId)); - dispatch(paymentInitializeState()); - - NavigationService.navigate(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.PAYMENT_TRANSACTION_SUMMARY, - params: { rptId: eitherRptId.right, startOrigin: "message" } - }); -}; diff --git a/ts/features/services/common/hooks/useFirstEffect.ts b/ts/features/services/common/hooks/useFirstEffect.ts new file mode 100644 index 00000000000..15e7db14021 --- /dev/null +++ b/ts/features/services/common/hooks/useFirstEffect.ts @@ -0,0 +1,17 @@ +import { useRef } from "react"; + +/** + * custom hook that returns true only on the first render + */ +export const useFirstEffect = () => { + const isFirstRender = useRef(true); + + if (isFirstRender.current) { + // eslint-disable-next-line functional/immutable-data + isFirstRender.current = false; + + return true; + } + + return isFirstRender.current; +}; diff --git a/ts/features/services/components/CardWithMarkdownContent.tsx b/ts/features/services/components/CardWithMarkdownContent.tsx new file mode 100644 index 00000000000..c61e48be5da --- /dev/null +++ b/ts/features/services/components/CardWithMarkdownContent.tsx @@ -0,0 +1,34 @@ +import React, { memo } from "react"; +import { StyleSheet, View } from "react-native"; +import { IOColors } from "@pagopa/io-app-design-system"; +import { Markdown } from "../../../components/ui/Markdown/Markdown"; + +const styles = StyleSheet.create({ + card: { + backgroundColor: IOColors.white, + borderRadius: 8, + borderWidth: 1, + borderColor: IOColors["grey-100"], + padding: 24 + } +}); + +export type CardWithMarkdownContentProps = { + content: string; +}; + +const CSS_STYLE = ` + body { + line-height: 1.5; + } +`; + +const CardWithMarkdownContent = memo( + ({ content }: CardWithMarkdownContentProps) => ( + + {content} + + ) +); + +export { CardWithMarkdownContent }; diff --git a/ts/features/services/components/ServiceDetailsFailure.tsx b/ts/features/services/components/ServiceDetailsFailure.tsx new file mode 100644 index 00000000000..199a8cc1219 --- /dev/null +++ b/ts/features/services/components/ServiceDetailsFailure.tsx @@ -0,0 +1,49 @@ +import React, { useCallback, useLayoutEffect } from "react"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import { useIODispatch } from "../../../store/hooks"; +import { loadServiceDetail } from "../../../store/actions/services"; +import I18n from "../../../i18n"; + +export type ServiceDetailsFailureProps = { + serviceId: string; +}; + +export const ServiceDetailsFailure = ({ + serviceId +}: ServiceDetailsFailureProps) => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false + }); + }, [navigation]); + + const handleBack = () => navigation.goBack(); + + const handleRetry = useCallback(() => { + dispatch(loadServiceDetail.request(serviceId)); + }, [dispatch, serviceId]); + + return ( + + ); +}; diff --git a/ts/features/services/components/ServiceDetailsHeader.tsx b/ts/features/services/components/ServiceDetailsHeader.tsx new file mode 100644 index 00000000000..80c1bd39fc5 --- /dev/null +++ b/ts/features/services/components/ServiceDetailsHeader.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { ImageURISource, StyleSheet, View } from "react-native"; +import { + Avatar, + H3, + IOSpacingScale, + IOStyles, + LabelSmall +} from "@pagopa/io-app-design-system"; + +const ITEM_PADDING_VERTICAL: IOSpacingScale = 6; +const AVATAR_MARGIN_RIGHT: IOSpacingScale = 16; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingVertical: ITEM_PADDING_VERTICAL + }, + itemAvatar: { + marginRight: AVATAR_MARGIN_RIGHT + } +}); + +export type ServiceDetailsHeaderProps = { + logoUri: ReadonlyArray; + organizationName: string; + serviceName: string; +}; + +export const ServiceDetailsHeader = ({ + logoUri, + organizationName, + serviceName +}: ServiceDetailsHeaderProps) => ( + + + + + +

{serviceName}

+ + {organizationName} + +
+
+); diff --git a/ts/features/services/components/ServiceDetailsPreferences.tsx b/ts/features/services/components/ServiceDetailsPreferences.tsx new file mode 100644 index 00000000000..35767a54364 --- /dev/null +++ b/ts/features/services/components/ServiceDetailsPreferences.tsx @@ -0,0 +1,162 @@ +import React, { ComponentProps, useCallback, useEffect } from "react"; +import { FlatList, ListRenderItemInfo } from "react-native"; +import { + Divider, + IOToast, + ListItemHeader, + ListItemSwitch +} from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import * as RA from "fp-ts/lib/ReadonlyArray"; +import { pipe } from "fp-ts/lib/function"; +import { NotificationChannelEnum } from "../../../../definitions/backend/NotificationChannel"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import I18n from "../../../i18n"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { isPremiumMessagesOptInOutEnabledSelector } from "../../../store/reducers/backendStatus"; +import { EnabledChannels } from "../../../utils/profile"; +import { useFirstEffect } from "../common/hooks/useFirstEffect"; +import { upsertServicePreference } from "../store/actions"; +import { + isErrorServicePreferenceSelector, + isLoadingServicePreferenceSelector, + servicePreferenceResponseSuccessSelector +} from "../store/reducers/servicePreference"; +import { serviceMetadataInfoSelector } from "../store/reducers/servicesById"; + +const hasChannel = ( + notificationChannel: NotificationChannelEnum, + channels: ReadonlyArray = [] +) => + pipe( + channels, + RA.findFirst(channel => channel === notificationChannel), + O.isSome + ); + +type PreferenceSwitchListItem = { + condition?: boolean; +} & ComponentProps; + +export type ServiceDetailsPreferencesProps = { + serviceId: ServiceId; + availableChannels?: ReadonlyArray; +}; + +export const ServiceDetailsPreferences = ({ + serviceId, + availableChannels = [] +}: ServiceDetailsPreferencesProps) => { + const isFirstRender = useFirstEffect(); + + const dispatch = useIODispatch(); + + const servicePreferenceResponseSuccess = useIOSelector( + servicePreferenceResponseSuccessSelector + ); + + const isLoadingServicePreference = useIOSelector( + isLoadingServicePreferenceSelector + ); + + const isErrorServicePreference = useIOSelector( + isErrorServicePreferenceSelector + ); + + const isPremiumMessagesOptInOutEnabled = useIOSelector( + isPremiumMessagesOptInOutEnabledSelector + ); + + const serviceMetadataInfo = useIOSelector(state => + serviceMetadataInfoSelector(state, serviceId) + ); + + const isInboxPreferenceEnabled = pipe( + servicePreferenceResponseSuccess, + O.fromNullable, + O.map(servicePreference => servicePreference.value.inbox), + O.getOrElse(() => false) + ); + + useEffect(() => { + if (!isFirstRender && isErrorServicePreference) { + IOToast.error(I18n.t("global.genericError")); + } + }, [isFirstRender, isErrorServicePreference]); + + const handleSwitchValueChange = useCallback( + (channel: keyof EnabledChannels, value: boolean) => { + if (servicePreferenceResponseSuccess) { + dispatch( + upsertServicePreference.request({ + id: serviceId, + ...servicePreferenceResponseSuccess.value, + [channel]: value + }) + ); + } + }, + [dispatch, serviceId, servicePreferenceResponseSuccess] + ); + + const preferenceListItems: ReadonlyArray = [ + // this switch is disabled if the current service is a special service. + // the user can enable the service only using the proper special service flow. + { + disabled: serviceMetadataInfo?.isSpecialService, + icon: "message", + isLoading: isLoadingServicePreference, + label: I18n.t("services.details.preferences.inbox"), + onSwitchValueChange: (value: boolean) => + handleSwitchValueChange("inbox", value), + value: servicePreferenceResponseSuccess?.value.inbox + }, + { + condition: + isInboxPreferenceEnabled && + hasChannel(NotificationChannelEnum.WEBHOOK, availableChannels), + icon: "bell", + isLoading: isLoadingServicePreference, + label: I18n.t("services.details.preferences.pushNotifications"), + onSwitchValueChange: (value: boolean) => + handleSwitchValueChange("push", value), + value: servicePreferenceResponseSuccess?.value.push + }, + { + condition: isInboxPreferenceEnabled && isPremiumMessagesOptInOutEnabled, + icon: "read", + isLoading: isLoadingServicePreference, + label: I18n.t("services.details.preferences.messageReadStatus"), + onSwitchValueChange: (value: boolean) => + handleSwitchValueChange("can_access_message_read_status", value), + value: + servicePreferenceResponseSuccess?.value.can_access_message_read_status + } + ]; + + const filteredPreferenceListItems = preferenceListItems.filter( + item => item.condition !== false + ); + + const renderItem = useCallback( + ({ + item: { condition, ...rest } + }: ListRenderItemInfo) => ( + + ), + [] + ); + + return ( + + } + ItemSeparatorComponent={() => } + data={filteredPreferenceListItems} + keyExtractor={item => item.label} + renderItem={renderItem} + scrollEnabled={false} + /> + ); +}; diff --git a/ts/features/services/components/__tests__/ServiceDetailsHeader.test.tsx b/ts/features/services/components/__tests__/ServiceDetailsHeader.test.tsx new file mode 100644 index 00000000000..3eb8775606b --- /dev/null +++ b/ts/features/services/components/__tests__/ServiceDetailsHeader.test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "@testing-library/react-native"; +import { ServiceDetailsHeader } from "../ServiceDetailsHeader"; + +describe("ServiceDetailsHeader component", () => { + it("should match the snapshot", () => { + const component = render( + + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/ts/features/services/components/__tests__/__snapshots__/ServiceDetailsHeader.test.tsx.snap b/ts/features/services/components/__tests__/__snapshots__/ServiceDetailsHeader.test.tsx.snap new file mode 100644 index 00000000000..644c9aef7c1 --- /dev/null +++ b/ts/features/services/components/__tests__/__snapshots__/ServiceDetailsHeader.test.tsx.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServiceDetailsHeader component should match the snapshot 1`] = ` + + + + + + + + + + + #### service name #### + + + #### organization_name #### + + + +`; diff --git a/ts/features/services/navigation/navigator.tsx b/ts/features/services/navigation/navigator.tsx new file mode 100644 index 00000000000..406fd332431 --- /dev/null +++ b/ts/features/services/navigation/navigator.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { createStackNavigator } from "@react-navigation/stack"; +import { isGestureEnabled } from "../../../utils/navigation"; +import { isNewServicesEnabled, myPortalEnabled } from "../../../config"; +import { ServiceDetailsScreen } from "../screens/ServiceDetailsScreen"; +import ServicesWebviewScreen from "../../../screens/services/ServicesWebviewScreen"; +import LegacyServiceDetailsScreen from "../../../screens/services/LegacyServiceDetailsScreen"; +import { ServicesParamsList } from "./params"; +import { SERVICES_ROUTES } from "./routes"; + +const Stack = createStackNavigator(); + +const ServicesNavigator = () => ( + + + {myPortalEnabled && ( + + )} + +); + +export default ServicesNavigator; diff --git a/ts/features/services/navigation/params.ts b/ts/features/services/navigation/params.ts new file mode 100644 index 00000000000..e5c39aebe86 --- /dev/null +++ b/ts/features/services/navigation/params.ts @@ -0,0 +1,8 @@ +import { ServiceDetailsScreenNavigationParams } from "../screens/ServiceDetailsScreen"; +import { ServiceWebviewScreenNavigationParams } from "../../../screens/services/ServicesWebviewScreen"; +import { SERVICES_ROUTES } from "./routes"; + +export type ServicesParamsList = { + [SERVICES_ROUTES.SERVICE_DETAIL]: ServiceDetailsScreenNavigationParams; + [SERVICES_ROUTES.SERVICE_WEBVIEW]: ServiceWebviewScreenNavigationParams; +}; diff --git a/ts/features/services/navigation/routes.ts b/ts/features/services/navigation/routes.ts new file mode 100644 index 00000000000..ac7a753757a --- /dev/null +++ b/ts/features/services/navigation/routes.ts @@ -0,0 +1,6 @@ +export const SERVICES_ROUTES = { + SERVICES_NAVIGATOR: "SERVICES_NAVIGATOR", + SERVICES_HOME: "SERVICES_HOME", + SERVICE_DETAIL: "SERVICE_DETAIL", + SERVICE_WEBVIEW: "SERVICE_WEBVIEW" +} as const; diff --git a/ts/sagas/services/servicePreference/handleGetServicePreferenceSaga.ts b/ts/features/services/saga/handleGetServicePreference.ts similarity index 75% rename from ts/sagas/services/servicePreference/handleGetServicePreferenceSaga.ts rename to ts/features/services/saga/handleGetServicePreference.ts index c8311e88d7c..5c025c282a7 100644 --- a/ts/sagas/services/servicePreference/handleGetServicePreferenceSaga.ts +++ b/ts/features/services/saga/handleGetServicePreference.ts @@ -1,12 +1,14 @@ import * as E from "fp-ts/lib/Either"; -import { call, put } from "typed-redux-saga/macro"; +import { call, put, select } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; import { BackendClient } from "../../../api/backend"; -import { loadServicePreference } from "../../../store/actions/services/servicePreference"; -import { ServicePreferenceResponseFailure } from "../../../types/services/ServicePreferenceResponse"; +import { loadServicePreference } from "../store/actions"; +import { ServicePreferenceResponseFailure } from "../types/ServicePreferenceResponse"; import { SagaCallReturnType } from "../../../types/utils"; import { getGenericError, getNetworkError } from "../../../utils/errors"; import { readablePrivacyReport } from "../../../utils/reporters"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { isFastLoginEnabledSelector } from "../../fastLogin/store/selectors"; export const mapKinds: Record< number, @@ -23,16 +25,25 @@ export const mapKinds: Record< * @param action */ export function* handleGetServicePreference( - getServicePreference: ReturnType< - typeof BackendClient - >["getServicePreference"], + getServicePreference: BackendClient["getServicePreference"], action: ActionType ) { try { const response: SagaCallReturnType = - yield* call(getServicePreference, { service_id: action.payload }); + (yield* call( + withRefreshApiCall, + getServicePreference({ service_id: action.payload }), + action + )) as unknown as SagaCallReturnType; if (E.isRight(response)) { + if (response.right.status === 401) { + const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); + if (isFastLoginEnabled) { + return; + } + } + if (response.right.status === 200) { yield* put( loadServicePreference.success({ diff --git a/ts/sagas/services/servicePreference/handleUpsertServicePreferenceSaga.ts b/ts/features/services/saga/handleUpsertServicePreference.ts similarity index 68% rename from ts/sagas/services/servicePreference/handleUpsertServicePreferenceSaga.ts rename to ts/features/services/saga/handleUpsertServicePreference.ts index a6d54d8bf0d..eb3542f4923 100644 --- a/ts/sagas/services/servicePreference/handleUpsertServicePreferenceSaga.ts +++ b/ts/features/services/saga/handleUpsertServicePreference.ts @@ -1,20 +1,26 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import { call, put, select } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; -import { ServicePreference } from "../../../../definitions/backend/ServicePreference"; import { PathTraversalSafePathParam } from "../../../../definitions/backend/PathTraversalSafePathParam"; +import { ServicePreference } from "../../../../definitions/backend/ServicePreference"; import { BackendClient } from "../../../api/backend"; -import { upsertServicePreference } from "../../../store/actions/services/servicePreference"; -import { - servicePreferenceSelector, - ServicePreferenceState -} from "../../../store/reducers/entities/services/servicePreference"; -import { isServicePreferenceResponseSuccess } from "../../../types/services/ServicePreferenceResponse"; import { SagaCallReturnType } from "../../../types/utils"; import { getGenericError, getNetworkError } from "../../../utils/errors"; import { readablePrivacyReport } from "../../../utils/reporters"; -import { mapKinds } from "./handleGetServicePreferenceSaga"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { isFastLoginEnabledSelector } from "../../fastLogin/store/selectors"; +import { trackPNPushSettings } from "../../pn/analytics"; +import { upsertServicePreference } from "../store/actions"; +import { + ServicePreferenceState, + servicePreferenceSelector +} from "../store/reducers/servicePreference"; +import { serviceMetadataInfoSelector } from "../store/reducers/servicesById"; +import { isServicePreferenceResponseSuccess } from "../types/ServicePreferenceResponse"; +import { mapKinds } from "./handleGetServicePreference"; /** * Generates the payload for the updating preferences request, if a users disables the inbox flag than the other flags @@ -47,6 +53,7 @@ const calculateUpdatingPreference = ( .settings_version as ServicePreference["settings_version"] }; } + return { is_inbox_enabled: action.payload.inbox, is_webhook_enabled: action.payload.inbox ? action.payload.push : false, @@ -59,17 +66,37 @@ const calculateUpdatingPreference = ( }; }; +export function* trackPNPushNotificationSettings( + action: ActionType +) { + const serviceMetadataInfo = yield* select( + serviceMetadataInfoSelector, + action.payload.id + ); + + pipe( + serviceMetadataInfo, + O.fromNullable, + O.chainNullableK(metadata => metadata.customSpecialFlow), + O.filter(customSpecialFlow => customSpecialFlow === "pn"), + O.fold( + () => undefined, + _ => trackPNPushSettings(action.payload.push) + ) + ); +} + /** * saga to handle the update of service preferences after a user specific action * @param upsertServicePreferences * @param action */ export function* handleUpsertServicePreference( - upsertServicePreferences: ReturnType< - typeof BackendClient - >["upsertServicePreference"], + upsertServicePreferences: BackendClient["upsertServicePreference"], action: ActionType ) { + yield* call(trackPNPushNotificationSettings, action); + const currentPreferences: ReturnType = yield* select(servicePreferenceSelector); @@ -79,21 +106,38 @@ export function* handleUpsertServicePreference( ); try { - const serviceIdEither = PathTraversalSafePathParam.decode( - action.payload.id - ); - - if (E.isLeft(serviceIdEither)) { - throw Error("Unable to decode ServiceId to PathTraversalSafePathParam"); + if (!PathTraversalSafePathParam.is(action.payload.id)) { + yield* put( + upsertServicePreference.failure({ + id: action.payload.id, + ...getGenericError( + new Error( + "Unable to decode ServiceId to PathTraversalSafePathParam" + ) + ) + }) + ); + return; } const response: SagaCallReturnType = - yield* call(upsertServicePreferences, { - service_id: serviceIdEither.right, - body: updatingPreference - }); + (yield* call( + withRefreshApiCall, + upsertServicePreferences({ + service_id: action.payload.id, + body: updatingPreference + }), + action + )) as unknown as SagaCallReturnType; if (E.isRight(response)) { + if (response.right.status === 401) { + const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); + if (isFastLoginEnabled) { + return; + } + } + if (response.right.status === 200) { yield* put( upsertServicePreference.success({ diff --git a/ts/features/services/screens/ServiceDetailsScreen.tsx b/ts/features/services/screens/ServiceDetailsScreen.tsx new file mode 100644 index 00000000000..dbea568bed8 --- /dev/null +++ b/ts/features/services/screens/ServiceDetailsScreen.tsx @@ -0,0 +1,192 @@ +import React, { useEffect, useMemo } from "react"; +import { Dimensions, StyleSheet, View } from "react-native"; +import { useHeaderHeight } from "@react-navigation/elements"; +import { + ContentWrapper, + IOColors, + IOVisualCostants, + VSpacer +} from "@pagopa/io-app-design-system"; +import Animated, { + useAnimatedScrollHandler, + useSharedValue +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { ServicesParamsList } from "../navigation/params"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; +import { + isErrorServiceByIdSelector, + isLoadingServiceByIdSelector, + serviceByIdSelector +} from "../store/reducers/servicesById"; +import { loadServiceDetail } from "../../../store/actions/services"; +import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { ServiceDetailsHeader } from "../components/ServiceDetailsHeader"; +import { logosForService } from "../../../utils/services"; +import { CardWithMarkdownContent } from "../components/CardWithMarkdownContent"; +import { ServiceDetailsFailure } from "../components/ServiceDetailsFailure"; +import { ServiceDetailsPreferences } from "../components/ServiceDetailsPreferences"; +import { loadServicePreference } from "../store/actions"; + +export type ServiceDetailsScreenNavigationParams = Readonly<{ + serviceId: ServiceId; + // if true the service should be activated automatically + // as soon as the screen is shown (used for custom activation + // flows like PN) + activate?: boolean; +}>; + +type ServiceDetailsScreenProps = IOStackNavigationRouteProps< + ServicesParamsList, + "SERVICE_DETAIL" +>; + +const headerPaddingBottom = 138; + +const styles = StyleSheet.create({ + scrollContentContainer: { + flexGrow: 1 + }, + headerContainer: { + backgroundColor: IOColors["grey-50"], + paddingBottom: headerPaddingBottom + }, + cardContainer: { + marginTop: -headerPaddingBottom, + minHeight: headerPaddingBottom + } +}); + +export const ServiceDetailsScreen = ({ route }: ServiceDetailsScreenProps) => { + const { serviceId } = route.params; + + const dispatch = useIODispatch(); + + useEffect(() => { + dispatch(loadServiceDetail.request(serviceId)); + dispatch(loadServicePreference.request(serviceId)); + }, [dispatch, serviceId]); + + const serviceById = useIOSelector(state => + serviceByIdSelector(state, serviceId) + ); + + const isLoadingServiceById = useIOSelector(state => + isLoadingServiceByIdSelector(state, serviceId) + ); + + const isErrorServiceById = useIOSelector(state => + isErrorServiceByIdSelector(state, serviceId) + ); + + if (!serviceById) { + return null; + } + + if (isErrorServiceById) { + return ; + } + + if (isLoadingServiceById) { + // TODO: add a loading screen + } + + return ; +}; + +const scrollTriggerOffsetValue: number = 88; +const windowHeight = Dimensions.get("window").height; + +type ServiceDetailsContentProps = { + service: ServicePublic; +}; + +const ServiceDetailsContent = ({ service }: ServiceDetailsContentProps) => { + const safeAreaInsets = useSafeAreaInsets(); + + const headerHeight = useHeaderHeight(); + const scrollTranslationY = useSharedValue(0); + + const safeBottomAreaHeight: number = useMemo( + () => + safeAreaInsets.bottom === 0 + ? IOVisualCostants.appMarginDefault + : safeAreaInsets.bottom, + [safeAreaInsets] + ); + + const { + organization_name, + service_id, + service_name, + available_notification_channels, + service_metadata + } = service; + + useHeaderSecondLevel({ + title: service_name, + supportRequest: true, + transparent: true, + scrollValues: { + triggerOffset: scrollTriggerOffsetValue, + contentOffsetY: scrollTranslationY + } + }); + + const scrollHandler = useAnimatedScrollHandler(({ contentOffset }) => { + // eslint-disable-next-line functional/immutable-data + scrollTranslationY.value = contentOffset.y; + }); + + return ( + + + + + + + + + + {service_metadata?.description && ( + + + + )} + + + + + + ); +}; diff --git a/ts/store/actions/services/servicePreference.ts b/ts/features/services/store/actions/index.ts similarity index 81% rename from ts/store/actions/services/servicePreference.ts rename to ts/features/services/store/actions/index.ts index 6dff87d49d9..6582981c613 100644 --- a/ts/store/actions/services/servicePreference.ts +++ b/ts/features/services/store/actions/index.ts @@ -3,9 +3,9 @@ import { ServicePreference, ServicePreferenceResponse, WithServiceID -} from "../../../types/services/ServicePreferenceResponse"; -import { NetworkError } from "../../../utils/errors"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; +} from "../../types/ServicePreferenceResponse"; +import { NetworkError } from "../../../../utils/errors"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; /** * Actions to load the specified preferences for a given ServiceID diff --git a/ts/features/services/store/reducers/__tests__/servicePreference.test.ts b/ts/features/services/store/reducers/__tests__/servicePreference.test.ts new file mode 100644 index 00000000000..a8c4158b059 --- /dev/null +++ b/ts/features/services/store/reducers/__tests__/servicePreference.test.ts @@ -0,0 +1,215 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { Action, createStore } from "redux"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { NetworkError, getNetworkError } from "../../../../../utils/errors"; +import { + ServicePreference, + ServicePreferenceResponse, + WithServiceID +} from "../../../types/ServicePreferenceResponse"; +import { loadServicePreference, upsertServicePreference } from "../../actions"; +import { + isErrorServicePreferenceSelector, + isLoadingServicePreferenceSelector, + servicePreferenceResponseSuccessSelector +} from "../servicePreference"; +import { GlobalState } from "../../../../../store/reducers/types"; + +const serviceId = "serviceId" as ServiceId; + +const servicePreferenceResponseSuccess: ServicePreferenceResponse = { + id: serviceId, + kind: "success", + value: { + inbox: true, + push: true, + email: false, + can_access_message_read_status: false, + settings_version: 0 + } +}; + +const servicePreferenceResponseFailure: ServicePreferenceResponse = { + id: serviceId, + kind: "notFound" +}; + +const servicePreferenceError: WithServiceID = { + id: serviceId, + ...getNetworkError(new Error("GenericError")) +}; + +const updatingResponse: WithServiceID = { + id: serviceId, + inbox: true, + push: true, + email: true, + can_access_message_read_status: true, + settings_version: 0 +}; + +describe("servicePreference reducer", () => { + it("should have initial state", () => { + const state = appReducer(undefined, applicationChangeState("active")); + + expect(state.entities.services.servicePreference).toStrictEqual(pot.none); + }); + + it("should handle loadServicePreference action", () => { + const state = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, state as any); + + store.dispatch(loadServicePreference.request(serviceId)); + + expect(store.getState().entities.services.servicePreference).toStrictEqual( + pot.noneLoading + ); + + store.dispatch( + loadServicePreference.success(servicePreferenceResponseSuccess) + ); + expect(store.getState().entities.services.servicePreference).toStrictEqual( + pot.some(servicePreferenceResponseSuccess) + ); + + store.dispatch(loadServicePreference.failure(servicePreferenceError)); + expect(store.getState().entities.services.servicePreference).toStrictEqual( + pot.someError(servicePreferenceResponseSuccess, servicePreferenceError) + ); + }); + + it("should handle upsertServicePreference action", () => { + const state = appReducer(undefined, applicationChangeState("active")); + const finalState: GlobalState = { + ...state, + entities: { + ...state.entities, + services: { + ...state.entities.services, + servicePreference: pot.some(servicePreferenceResponseSuccess) + } + } + }; + const store = createStore(appReducer, finalState as any); + + store.dispatch(upsertServicePreference.request(updatingResponse)); + + expect(store.getState().entities.services.servicePreference).toStrictEqual( + pot.someUpdating(servicePreferenceResponseSuccess, { + id: serviceId, + kind: "success", + value: { + inbox: true, + push: true, + email: true, + can_access_message_read_status: true, + settings_version: 0 + } + }) + ); + + store.dispatch(upsertServicePreference.failure(servicePreferenceError)); + expect(store.getState().entities.services.servicePreference).toStrictEqual( + pot.someError(servicePreferenceResponseSuccess, servicePreferenceError) + ); + }); +}); + +describe("servicePreference selectors", () => { + describe("servicePreferenceResponseSuccessSelector", () => { + it("should return servicePreferenceResponseSuccess when pot.some and the service preference is successfully loaded", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.success(servicePreferenceResponseSuccess) + ); + const servicePreferenceResponse = + servicePreferenceResponseSuccessSelector(state); + expect(servicePreferenceResponse).toStrictEqual( + servicePreferenceResponseSuccess + ); + }); + + it("should return undefined when pot.some and the service preference is NOT successfully loaded", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.success(servicePreferenceResponseFailure) + ); + const servicePreferenceResponse = + servicePreferenceResponseSuccessSelector(state); + expect(servicePreferenceResponse).toBeUndefined(); + }); + }); + + describe("isLoadingServicePreferenceSelector", () => { + it("should return true when pot.loading", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.request(serviceId) + ); + + const isLoadingServicePreference = + isLoadingServicePreferenceSelector(state); + expect(isLoadingServicePreference).toStrictEqual(true); + }); + + it("should return true when pot.updating", () => { + const state = appReducer( + {} as GlobalState, + upsertServicePreference.request(updatingResponse) + ); + + const isLoadingServicePreference = + isLoadingServicePreferenceSelector(state); + expect(isLoadingServicePreference).toStrictEqual(true); + }); + + it("should return false when not pot.some", () => { + expect( + isLoadingServicePreferenceSelector(appReducer(undefined, {} as Action)) + ).toStrictEqual(false); + + expect( + isLoadingServicePreferenceSelector( + appReducer( + {} as GlobalState, + loadServicePreference.failure(servicePreferenceError) + ) + ) + ).toStrictEqual(false); + }); + }); + + describe("isErrorServicePreferenceSelector", () => { + it("should return true when pot.error", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.failure(servicePreferenceError) + ); + + const isErrorServicePreference = isErrorServicePreferenceSelector(state); + expect(isErrorServicePreference).toStrictEqual(true); + }); + + it("should return true when pot.some and service is not successfully loaded", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.success(servicePreferenceResponseFailure) + ); + + const isErrorServicePreference = isErrorServicePreferenceSelector(state); + expect(isErrorServicePreference).toStrictEqual(true); + }); + + it("should return false when pot.some and service is successfully loaded", () => { + const state = appReducer( + {} as GlobalState, + loadServicePreference.success(servicePreferenceResponseSuccess) + ); + + const isErrorServicePreference = isErrorServicePreferenceSelector(state); + expect(isErrorServicePreference).toStrictEqual(false); + }); + }); +}); diff --git a/ts/features/services/store/reducers/__tests__/servicesById.test.ts b/ts/features/services/store/reducers/__tests__/servicesById.test.ts new file mode 100644 index 00000000000..f24fd6834b4 --- /dev/null +++ b/ts/features/services/store/reducers/__tests__/servicesById.test.ts @@ -0,0 +1,317 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { + NonEmptyString, + OrganizationFiscalCode +} from "@pagopa/ts-commons/lib/strings"; +import { Tuple2 } from "@pagopa/ts-commons/lib/tuples"; +import { Action, createStore } from "redux"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; +import { ServiceScopeEnum } from "../../../../../../definitions/backend/ServiceScope"; +import { SpecialServiceCategoryEnum } from "../../../../../../definitions/backend/SpecialServiceCategory"; +import { StandardServiceCategoryEnum } from "../../../../../../definitions/backend/StandardServiceCategory"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { + logoutSuccess, + sessionExpired +} from "../../../../../store/actions/authentication"; +import { + loadServiceDetail, + removeServiceTuples +} from "../../../../../store/actions/services"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { reproduceSequence } from "../../../../../utils/tests"; +import { + isErrorServiceByIdSelector, + isLoadingServiceByIdSelector, + serviceByIdSelector, + serviceMetadataByIdSelector, + serviceMetadataInfoSelector +} from "../servicesById"; + +const serviceId = "serviceId" as ServiceId; + +const service = { + service_id: serviceId, + service_name: "health", + organization_name: "Ċentru tas-Saħħa", + department_name: "covid-19", + organization_fiscal_code: "FSCLCD" as OrganizationFiscalCode +} as ServicePublic; + +describe("serviceById reducer", () => { + it("should have initial state", () => { + const state = appReducer(undefined, applicationChangeState("active")); + + expect(state.entities.services.byId).toStrictEqual({}); + }); + + it("should handle loadServiceDetail action", () => { + const state = appReducer(undefined, applicationChangeState("active")); + const store = createStore(appReducer, state as any); + + store.dispatch(loadServiceDetail.request(serviceId)); + + expect(store.getState().entities.services.byId).toStrictEqual({ + serviceId: pot.noneLoading + }); + + store.dispatch(loadServiceDetail.success(service)); + expect(store.getState().entities.services.byId).toStrictEqual({ + serviceId: pot.some(service) + }); + + const tError = { + error: new Error("load failed"), + service_id: serviceId + }; + + store.dispatch(loadServiceDetail.failure(tError)); + expect(store.getState().entities.services.byId).toStrictEqual({ + serviceId: pot.someError(service, new Error("load failed")) + }); + }); + + it("should handle logoutSuccess action", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success(service), + logoutSuccess() + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + expect(state.entities.services.byId).toEqual({}); + }); + + it("should handle sessionExpired action", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success(service), + sessionExpired() + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + expect(state.entities.services.byId).toEqual({}); + }); + + it("should handle removeServiceTuples action", () => { + const sequenceOfActions: ReadonlyArray = [ + applicationChangeState("active"), + loadServiceDetail.success({ ...service, service_id: "s1" as ServiceId }), + loadServiceDetail.success({ ...service, service_id: "s2" as ServiceId }), + loadServiceDetail.success({ ...service, service_id: "s3" as ServiceId }), + loadServiceDetail.success({ ...service, service_id: "s4" as ServiceId }), + loadServiceDetail.success({ ...service, service_id: "s5" as ServiceId }), + removeServiceTuples([ + Tuple2("s2", "FSCLCD"), + Tuple2("s3", "FSCLCD"), + // Not existing serviceId + Tuple2("s6", "FSCLCD") + ]) + ]; + + const state: GlobalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + expect(state.entities.services.byId).toEqual({ + s1: pot.some({ ...service, service_id: "s1" as ServiceId }), + s4: pot.some({ ...service, service_id: "s4" as ServiceId }), + s5: pot.some({ ...service, service_id: "s5" as ServiceId }) + }); + }); +}); + +describe("serviceById selectors", () => { + describe("serviceByIdSelector", () => { + it("should return the ServicePublic when pot.some", () => { + const serviceById = serviceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.success(service)), + serviceId + ); + expect(serviceById).toStrictEqual(service); + }); + + it("should return undefined when not pot.some", () => { + expect( + serviceByIdSelector(appReducer(undefined, {} as Action), serviceId) + ).toBeUndefined(); + + expect( + serviceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.request(serviceId)), + serviceId + ) + ).toBeUndefined(); + + const tError = { + error: new Error("error"), + service_id: serviceId + }; + + expect( + serviceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.failure(tError)), + serviceId + ) + ).toBeUndefined(); + }); + }); + + describe("isLoadingServiceByIdSelector", () => { + it("should return true when pot.loading", () => { + const isLoadingServiceById = isLoadingServiceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.request(serviceId)), + serviceId + ); + expect(isLoadingServiceById).toStrictEqual(true); + }); + + it("should return false when not pot.some", () => { + expect( + isLoadingServiceByIdSelector( + appReducer(undefined, {} as Action), + serviceId + ) + ).toStrictEqual(false); + + expect( + isLoadingServiceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.success(service)), + serviceId + ) + ).toStrictEqual(false); + }); + }); + + describe("isErrorServiceByIdSelector", () => { + it("should return true when pot.error", () => { + const tError = { + error: new Error("error"), + service_id: serviceId + }; + + const isErrorServiceById = isErrorServiceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.failure(tError)), + serviceId + ); + expect(isErrorServiceById).toStrictEqual(true); + }); + + it("should return false when not pot.error", () => { + expect( + isErrorServiceByIdSelector( + appReducer(undefined, {} as Action), + serviceId + ) + ).toStrictEqual(false); + + expect( + isErrorServiceByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.success(service)), + serviceId + ) + ).toStrictEqual(false); + }); + }); + + describe("serviceMetadataByIdSelector", () => { + it("should return ServiceMetadata when pot.some and service_metadata defined", () => { + const serviceById = serviceMetadataByIdSelector( + appReducer( + {} as GlobalState, + loadServiceDetail.success({ + ...service, + service_metadata: { + category: StandardServiceCategoryEnum.STANDARD, + scope: ServiceScopeEnum.LOCAL + } + }) + ), + serviceId + ); + expect(serviceById).toStrictEqual({ + category: StandardServiceCategoryEnum.STANDARD, + scope: ServiceScopeEnum.LOCAL + }); + }); + + it("should return undefined when pot.some and service_metadata not defined", () => { + const serviceById = serviceMetadataByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.success(service)), + serviceId + ); + expect(serviceById).toBeUndefined(); + }); + + it("should return undefined when not pot.some", () => { + expect( + serviceMetadataByIdSelector( + appReducer(undefined, {} as Action), + serviceId + ) + ).toBeUndefined(); + + expect( + serviceMetadataByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.request(serviceId)), + serviceId + ) + ).toBeUndefined(); + + const tError = { + error: new Error("error"), + service_id: serviceId + }; + + expect( + serviceMetadataByIdSelector( + appReducer({} as GlobalState, loadServiceDetail.failure(tError)), + serviceId + ) + ).toBeUndefined(); + }); + }); + + describe("serviceMetadataInfoSelector", () => { + it("should return serviceMetadataInfo when pot.some and it is a special service", () => { + const serviceById = serviceMetadataInfoSelector( + appReducer( + {} as GlobalState, + loadServiceDetail.success({ + ...service, + service_metadata: { + category: SpecialServiceCategoryEnum.SPECIAL, + scope: ServiceScopeEnum.NATIONAL, + custom_special_flow: "custom_special_flow" as NonEmptyString + } + }) + ), + serviceId + ); + expect(serviceById).toStrictEqual({ + isSpecialService: true, + customSpecialFlow: "custom_special_flow" + }); + }); + + it("should return undefined when not pot.some and it is not a special service", () => { + const serviceById = serviceMetadataInfoSelector( + appReducer({} as GlobalState, loadServiceDetail.success(service)), + serviceId + ); + expect(serviceById).toBeUndefined(); + }); + }); +}); diff --git a/ts/features/services/store/reducers/servicePreference.ts b/ts/features/services/store/reducers/servicePreference.ts new file mode 100644 index 00000000000..69151eb48dd --- /dev/null +++ b/ts/features/services/store/reducers/servicePreference.ts @@ -0,0 +1,82 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { getType } from "typesafe-actions"; +import { + ServicePreferenceResponse, + WithServiceID, + isServicePreferenceResponseSuccess +} from "../../types/ServicePreferenceResponse"; +import { NetworkError } from "../../../../utils/errors"; +import { isStrictSome } from "../../../../utils/pot"; +import { loadServicePreference, upsertServicePreference } from "../actions"; +import { Action } from "../../../../store/actions/types"; +import { GlobalState } from "../../../../store/reducers/types"; + +export type ServicePreferenceState = pot.Pot< + ServicePreferenceResponse, + WithServiceID +>; + +const INITIAL_STATE: ServicePreferenceState = pot.none; + +// Reducer to handle specific service contact preferences (inbox, push, emails) +const servicePreferenceReducer = ( + state: ServicePreferenceState = INITIAL_STATE, + action: Action +): ServicePreferenceState => { + switch (action.type) { + case getType(loadServicePreference.request): + return pot.toLoading(state); + case getType(upsertServicePreference.request): + const { id, ...payload } = action.payload; + + return pot.toUpdating(state, { + id, + kind: "success", + value: payload + }); + case getType(loadServicePreference.success): + case getType(upsertServicePreference.success): + return pot.some(action.payload); + case getType(loadServicePreference.failure): + case getType(upsertServicePreference.failure): + return pot.toError(state, action.payload); + } + return state; +}; + +export default servicePreferenceReducer; + +// Selectors +export const servicePreferenceSelector = ( + state: GlobalState +): ServicePreferenceState => state.entities.services.servicePreference; + +export const servicePreferenceResponseSuccessSelector = (state: GlobalState) => + pipe( + state, + servicePreferenceSelector, + pot.toOption, + O.filter(isServicePreferenceResponseSuccess), + O.toUndefined + ); + +export const isLoadingServicePreferenceSelector = (state: GlobalState) => + pipe( + state, + servicePreferenceSelector, + servicePreferencePot => + pot.isLoading(servicePreferencePot) || + pot.isUpdating(servicePreferencePot) + ); + +export const isErrorServicePreferenceSelector = (state: GlobalState) => + pipe( + state, + servicePreferenceSelector, + servicePreferencePot => + pot.isError(servicePreferencePot) || + (isStrictSome(servicePreferencePot) && + !isServicePreferenceResponseSuccess(servicePreferencePot.value)) + ); diff --git a/ts/store/reducers/entities/services/servicesById.ts b/ts/features/services/store/reducers/servicesById.ts similarity index 51% rename from ts/store/reducers/entities/services/servicesById.ts rename to ts/features/services/store/reducers/servicesById.ts index 3eefa95bdae..730db0d1328 100644 --- a/ts/store/reducers/entities/services/servicesById.ts +++ b/ts/features/services/store/reducers/servicesById.ts @@ -3,18 +3,24 @@ * It only manages SUCCESS actions because all UI state properties (like * loading/error) * are managed by different global reducers. */ -import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as pot from "@pagopa/ts-commons/lib/pot"; import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; -import { logoutSuccess, sessionExpired } from "../../../actions/authentication"; +import { + logoutSuccess, + sessionExpired +} from "../../../../store/actions/authentication"; import { loadServiceDetail, removeServiceTuples -} from "../../../actions/services"; -import { Action } from "../../../actions/types"; -import { GlobalState } from "../../types"; +} from "../../../../store/actions/services"; +import { Action } from "../../../../store/actions/types"; +import { GlobalState } from "../../../../store/reducers/types"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { ServiceMetadata } from "../../../../../definitions/backend/ServiceMetadata"; +import { SpecialServiceMetadata } from "../../../../../definitions/backend/SpecialServiceMetadata"; export type ServicesByIdState = Readonly<{ [key: string]: pot.Pot | undefined; @@ -22,7 +28,7 @@ export type ServicesByIdState = Readonly<{ const INITIAL_STATE: ServicesByIdState = {}; -const reducer = ( +const serviceByIdReducer = ( state: ServicesByIdState = INITIAL_STATE, action: Action ): ServicesByIdState => { @@ -30,15 +36,13 @@ const reducer = ( case getType(loadServiceDetail.request): // When a previously loaded service detail is loaded again, its state // is updated with a someLoading pot, otherwise its state is updated with a noneLoading pot - const cachedValue = state[action.payload]; - const prevServiceRequest = - cachedValue && pot.isSome(cachedValue) && !pot.isLoading(cachedValue) - ? pot.someLoading(cachedValue.value) - : pot.noneLoading; - return { ...state, - [action.payload]: prevServiceRequest + [action.payload]: pipe( + state[action.payload], + O.fromNullable, + O.fold(() => pot.noneLoading, pot.toLoading) + ) }; case getType(loadServiceDetail.success): @@ -47,22 +51,20 @@ const reducer = ( ...state, [action.payload.service_id]: pot.some(action.payload) }; - case getType(logoutSuccess): - case getType(sessionExpired): - return INITIAL_STATE; case getType(loadServiceDetail.failure): // when a request to load a previously loaded service detail fails its state is updated // with a someError pot, otherwise its state is updated with a noneError pot - const { service_id, error } = action.payload; - const prevServiceFailure = state[service_id]; - const nextServiceFailure = - prevServiceFailure !== undefined && pot.isSome(prevServiceFailure) - ? pot.someError(prevServiceFailure.value, error) - : pot.noneError(error); return { ...state, - [service_id]: nextServiceFailure + [action.payload.service_id]: pipe( + state[action.payload.service_id], + O.fromNullable, + O.fold( + () => pot.noneError(action.payload.error), + servicePot => pot.toError(servicePot, action.payload.error) + ) + ) }; case getType(removeServiceTuples): { @@ -73,25 +75,67 @@ const reducer = ( return newState; } + case getType(logoutSuccess): + case getType(sessionExpired): + return INITIAL_STATE; + default: return state; } }; +export default serviceByIdReducer; + // Selectors export const servicesByIdSelector = (state: GlobalState): ServicesByIdState => state.entities.services.byId; -export const serviceByIdSelector = ( +export const serviceByIdPotSelector = ( state: GlobalState, id: ServiceId ): pot.Pot => state.entities.services.byId[id] ?? pot.none; -export const serviceMetadataByIdSelector = - (id: ServiceId) => - (state: GlobalState): ServiceMetadata | undefined => { - const maybeServiceById = serviceByIdSelector(state, id); - return pot.toUndefined(maybeServiceById)?.service_metadata; - }; -export default reducer; +export const serviceByIdSelector = ( + state: GlobalState, + id: ServiceId +): ServicePublic | undefined => + pipe(serviceByIdPotSelector(state, id), pot.toUndefined); + +export const isLoadingServiceByIdSelector = ( + state: GlobalState, + id: ServiceId +) => pipe(serviceByIdPotSelector(state, id), pot.isLoading); + +export const isErrorServiceByIdSelector = (state: GlobalState, id: ServiceId) => + pipe(serviceByIdPotSelector(state, id), pot.isError); + +export const serviceMetadataByIdSelector = ( + state: GlobalState, + id: ServiceId +): ServiceMetadata | undefined => + pipe( + serviceByIdPotSelector(state, id), + pot.toOption, + O.chainNullableK(service => service.service_metadata), + O.toUndefined + ); + +export const serviceMetadataInfoSelector = ( + state: GlobalState, + id: ServiceId +) => + pipe( + serviceMetadataByIdSelector(state, id), + O.fromNullable, + O.chain(serviceMetadata => { + if (SpecialServiceMetadata.is(serviceMetadata)) { + return O.some({ + isSpecialService: true, + customSpecialFlow: serviceMetadata.custom_special_flow + }); + } + return O.none; + }), + O.toUndefined + ); diff --git a/ts/types/services/ServicePreferenceResponse.ts b/ts/features/services/types/ServicePreferenceResponse.ts similarity index 91% rename from ts/types/services/ServicePreferenceResponse.ts rename to ts/features/services/types/ServicePreferenceResponse.ts index 3d6f87527ff..dd324861c56 100644 --- a/ts/types/services/ServicePreferenceResponse.ts +++ b/ts/features/services/types/ServicePreferenceResponse.ts @@ -1,5 +1,5 @@ -import { EnabledChannels } from "../../utils/profile"; -import { ServiceId } from "../../../definitions/backend/ServiceId"; +import { EnabledChannels } from "../../../utils/profile"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; export type ServicePreference = { settings_version: number } & EnabledChannels; diff --git a/ts/features/wallet/bancomat/component/bancomatCard/BancomatCard.tsx b/ts/features/wallet/bancomat/component/bancomatCard/BancomatCard.tsx deleted file mode 100644 index 95318dd52b0..00000000000 --- a/ts/features/wallet/bancomat/component/bancomatCard/BancomatCard.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; -import * as React from "react"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { profileNameSurnameSelector } from "../../../../../store/reducers/profile"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { BancomatPaymentMethod } from "../../../../../types/pagopa"; -import { isPaymentMethodExpired } from "../../../../../utils/paymentMethod"; -import BaseBancomatCard from "./BaseBancomatCard"; - -type OwnProps = { enhancedBancomat: BancomatPaymentMethod }; - -type Props = ReturnType & - ReturnType & - OwnProps; - -const getExpireDate = (fullYear?: string, month?: string): Date | undefined => { - if (!fullYear || !month) { - return undefined; - } - const year = parseInt(fullYear, 10); - const indexedMonth = parseInt(month, 10); - if (isNaN(year) || isNaN(indexedMonth)) { - return undefined; - } - return new Date(year, indexedMonth - 1); -}; - -/** - * Render a bancomat already added to the wallet - * @param props - * @constructor - */ -const BancomatCard: React.FunctionComponent = props => ( - false) - )} - expiringDate={getExpireDate( - props.enhancedBancomat.info.expireYear, - props.enhancedBancomat.info.expireMonth - )} - user={props.nameSurname ?? ""} - /> -); - -const mapDispatchToProps = (_: Dispatch) => ({}); - -const mapStateToProps = (state: GlobalState) => ({ - nameSurname: profileNameSurnameSelector(state) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(BancomatCard); diff --git a/ts/features/wallet/bancomat/component/bancomatCard/BaseBancomatCard.tsx b/ts/features/wallet/bancomat/component/bancomatCard/BaseBancomatCard.tsx deleted file mode 100644 index 36f578f88b6..00000000000 --- a/ts/features/wallet/bancomat/component/bancomatCard/BaseBancomatCard.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { - View, - Image, - ImageStyle, - Platform, - StyleProp, - StyleSheet -} from "react-native"; -import { widthPercentageToDP } from "react-native-responsive-screen"; -import { IOColors, hexToRgba, VSpacer } from "@pagopa/io-app-design-system"; -import { Abi } from "../../../../../../definitions/pagopa/walletv2/Abi"; -import pagoBancomatLogo from "../../../../../../img/wallet/cards-icons/pagobancomat.png"; -import { IOBadge } from "../../../../../components/core/IOBadge"; -import { Body } from "../../../../../components/core/typography/Body"; -import { H5 } from "../../../../../components/core/typography/H5"; -import I18n from "../../../../../i18n"; -import customVariables from "../../../../../theme/variables"; -import { localeDateFormat } from "../../../../../utils/locale"; -import { BrandImage } from "../../../component/card/BrandImage"; -import { useImageResize } from "../../../onboarding/bancomat/hooks/useImageResize"; - -type Props = { - abi: Abi; - expiringDate?: Date; - isExpired?: boolean; - user: string; - blocked?: boolean; - accessibilityLabel?: string; -}; - -const BASE_IMG_W = 160; -const BASE_IMG_H = 40; -const opaqueBorderColor = hexToRgba(IOColors.black, 0.1); - -const styles = StyleSheet.create({ - cardBox: { - height: 192, - width: widthPercentageToDP("90%"), - maxWidth: 343, - paddingHorizontal: customVariables.contentPadding, - paddingTop: 20, - paddingBottom: 22, - flexDirection: "column", - justifyContent: "space-between", - backgroundColor: IOColors.greyUltraLight, - borderRadius: 8, - shadowColor: IOColors.black, - shadowOffset: { - width: 0, - height: 3 - }, - shadowOpacity: 0.18, - shadowRadius: 4.65, - zIndex: 7, - elevation: 7 - }, - shadowBox: { - marginBottom: -13, - borderRadius: 8, - borderTopWidth: 10, - borderTopColor: opaqueBorderColor, - height: 15, - width: widthPercentageToDP("90%"), - maxWidth: 343 - }, - bottomRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "baseline" - } -}); - -/** - * Generate the accessibility label for the card. - */ -const getAccessibilityRepresentation = ( - bankName: string, - expiringDate?: Date, - holder?: string -) => { - const cardRepresentation = I18n.t("wallet.accessibility.folded.bancomat", { - bankName - }); - - const computedValidity = - expiringDate !== undefined - ? `, ${I18n.t("cardComponent.validUntil")} ${localeDateFormat( - expiringDate, - I18n.t("global.dateFormats.numericMonthYear") - )}` - : ""; - - const computedHolder = - holder !== undefined - ? `, ${I18n.t("wallet.accessibility.cardHolder")} ${holder}` - : ""; - - return `${cardRepresentation}${computedValidity}${computedHolder}`; -}; - -/** - * Render the image (if available) or the bank name (if available) - * or the generic bancomat string (final fallback). - * @param abi - * @param size - * TODO: refactor with {@link BancomatWalletPreview} - */ -const renderBankLogo = (abi: Abi, size: O.Option<[number, number]>) => - pipe( - size, - O.fold( - () => ( - - {abi.name ?? I18n.t("wallet.methods.bancomat.name")} - - ), - imgDim => { - const imageUrl = abi.logoUrl; - const imageStyle: StyleProp = { - width: imgDim[0], - height: imgDim[1], - resizeMode: "contain" - }; - return imageUrl ? ( - - ) : null; - } - ) - ); - -/** - * The base component that represents a full bancomat card - * @param props - * @constructor - */ -const BaseBancomatCard: React.FunctionComponent = (props: Props) => { - const imgDimensions = useImageResize( - BASE_IMG_W, - BASE_IMG_H, - props.abi?.logoUrl - ); - return ( - <> - {Platform.OS === "android" && } - - - - {renderBankLogo(props.abi, imgDimensions)} - {props.blocked && ( - - )} - - - {props.expiringDate && ( -
{`${I18n.t("cardComponent.validUntil")} ${localeDateFormat( - props.expiringDate, - I18n.t("global.dateFormats.numericMonthYear") - )}`}
- )} -
- - {props.user.toLocaleUpperCase()} - - -
- - ); -}; - -export default BaseBancomatCard; diff --git a/ts/features/wallet/bancomat/component/bancomatCard/PreviewBancomatCard.tsx b/ts/features/wallet/bancomat/component/bancomatCard/PreviewBancomatCard.tsx deleted file mode 100644 index 74991c073bc..00000000000 --- a/ts/features/wallet/bancomat/component/bancomatCard/PreviewBancomatCard.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from "react"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { Card } from "../../../../../../definitions/pagopa/walletv2/Card"; -import { profileNameSurnameSelector } from "../../../../../store/reducers/profile"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { Abi } from "../../../../../../definitions/pagopa/walletv2/Abi"; -import { isBancomatBlocked } from "../../../../../utils/paymentMethod"; -import BaseBancomatCard from "./BaseBancomatCard"; - -type OnboardingData = { bancomat: Card; abi: Abi }; - -type Props = ReturnType & - ReturnType & - OnboardingData; - -/** - * Display a preview of a bancomat that the user could add to the wallet - * @constructor - */ -const PreviewBancomatCard: React.FunctionComponent = props => ( - -); - -const mapDispatchToProps = (_: Dispatch) => ({}); - -const mapStateToProps = (state: GlobalState) => ({ - nameSurname: profileNameSurnameSelector(state) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(PreviewBancomatCard); diff --git a/ts/features/wallet/bancomat/screen/BancomatDetailScreen.tsx b/ts/features/wallet/bancomat/screen/BancomatDetailScreen.tsx index 3b60b9efad1..9a1e3730fe0 100644 --- a/ts/features/wallet/bancomat/screen/BancomatDetailScreen.tsx +++ b/ts/features/wallet/bancomat/screen/BancomatDetailScreen.tsx @@ -4,35 +4,33 @@ import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; +import { Route, useRoute } from "@react-navigation/native"; import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; -import { PaymentCardBig } from "../../../../components/ui/cards/payment/PaymentCardBig"; import I18n from "../../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../../navigation/params/WalletParamsList"; import { useIOSelector } from "../../../../store/hooks"; import { profileNameSurnameSelector } from "../../../../store/reducers/profile"; import { paymentMethodByIdSelector } from "../../../../store/reducers/wallet/wallets"; import { BancomatPaymentMethod, isBancomat } from "../../../../types/pagopa"; import BasePaymentMethodScreen from "../../common/BasePaymentMethodScreen"; import { acceptedPaymentMethodsFaqUrl } from "../../../../urls"; +import { PaymentCardBig } from "../../../payments/common/components/PaymentCardBig"; export type BancomatDetailScreenNavigationParams = Readonly<{ // TODO: we should use only the id and retrieve it from the store, otherwise we lose all the updates bancomat: BancomatPaymentMethod; }>; -type Props = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_BANCOMAT_DETAIL" ->; - /** * Detail screen for a bancomat * @constructor */ -const BancomatDetailScreen = ({ route }: Props) => { +const BancomatDetailScreen = () => { + const { idWallet } = + useRoute< + Route<"WALLET_BANCOMAT_DETAIL", BancomatDetailScreenNavigationParams> + >().params.bancomat; const bancomat = useIOSelector(state => - paymentMethodByIdSelector(state, route.params.bancomat.idWallet) + paymentMethodByIdSelector(state, idWallet) ); const bannerViewRef = React.useRef(null); const nameSurname = useIOSelector(profileNameSurnameSelector); diff --git a/ts/features/wallet/bancomatpay/screen/BPayDetailScreen.tsx b/ts/features/wallet/bancomatpay/screen/BPayDetailScreen.tsx index 302390dfaec..3aba20d6c08 100644 --- a/ts/features/wallet/bancomatpay/screen/BPayDetailScreen.tsx +++ b/ts/features/wallet/bancomatpay/screen/BPayDetailScreen.tsx @@ -4,16 +4,16 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; -import { - PaymentCardBig, - PaymentCardBigProps -} from "../../../../components/ui/cards/payment/PaymentCardBig"; import { useIOSelector } from "../../../../store/hooks"; import { paymentMethodByIdSelector } from "../../../../store/reducers/wallet/wallets"; import { BPayPaymentMethod, isBPay } from "../../../../types/pagopa"; import BasePaymentMethodScreen from "../../common/BasePaymentMethodScreen"; import PaymentMethodFeatures from "../../component/features/PaymentMethodFeatures"; import { profileNameSurnameSelector } from "../../../../store/reducers/profile"; +import { + PaymentCardBig, + PaymentCardBigProps +} from "../../../payments/common/components/PaymentCardBig"; export type BPayDetailScreenNavigationParams = Readonly<{ // TODO: we should use only the id and retrieve it from the store, otherwise we lose all the updates diff --git a/ts/features/wallet/cobadge/screen/CobadgeDetailScreen.tsx b/ts/features/wallet/cobadge/screen/CobadgeDetailScreen.tsx index 05b226a6dd2..4da614806bc 100644 --- a/ts/features/wallet/cobadge/screen/CobadgeDetailScreen.tsx +++ b/ts/features/wallet/cobadge/screen/CobadgeDetailScreen.tsx @@ -4,34 +4,31 @@ import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; +import { Route, useRoute } from "@react-navigation/native"; import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; -import { PaymentCardBig } from "../../../../components/ui/cards/payment/PaymentCardBig"; import I18n from "../../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../../navigation/params/WalletParamsList"; import { useIOSelector } from "../../../../store/hooks"; import { creditCardByIdSelector } from "../../../../store/reducers/wallet/wallets"; import { CreditCardPaymentMethod } from "../../../../types/pagopa"; import { acceptedPaymentMethodsFaqUrl } from "../../../../urls"; import { isCobadge } from "../../../../utils/paymentMethodCapabilities"; import BasePaymentMethodScreen from "../../common/BasePaymentMethodScreen"; +import { PaymentCardBig } from "../../../payments/common/components/PaymentCardBig"; export type CobadgeDetailScreenNavigationParams = Readonly<{ // TODO: we should use only the id and retrieve it from the store, otherwise we lose all the updates cobadge: CreditCardPaymentMethod; }>; -type Props = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_COBADGE_DETAIL" ->; - /** * Detail screen for a cobadge card * @constructor */ -const CobadgeDetailScreen = (props: Props) => { - const { cobadge } = props.route.params; +const CobadgeDetailScreen = () => { + const { cobadge } = + useRoute< + Route<"WALLET_COBADGE_DETAIL", CobadgeDetailScreenNavigationParams> + >().params; const card = useIOSelector(state => creditCardByIdSelector(state, cobadge.idWallet) ); diff --git a/ts/features/wallet/common/BasePaymentMethodScreen.tsx b/ts/features/wallet/common/BasePaymentMethodScreen.tsx index b238fe09ce1..eb5909857e8 100644 --- a/ts/features/wallet/common/BasePaymentMethodScreen.tsx +++ b/ts/features/wallet/common/BasePaymentMethodScreen.tsx @@ -13,10 +13,10 @@ import { IOColors, VSpacer, IOSpacingScale, - ListItemAction + ListItemAction, + useIOToast } from "@pagopa/io-app-design-system"; import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay"; -import { useIOToast } from "../../../components/Toast"; import { IOStyles } from "../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import FocusAwareStatusBar from "../../../components/ui/FocusAwareStatusBar"; @@ -115,7 +115,10 @@ const BasePaymentMethodScreen = (props: Props) => { dark={true} headerBackgroundColor={blueHeaderColor} > - + {card} diff --git a/ts/features/wallet/component/card/FeaturedCardCarousel.tsx b/ts/features/wallet/component/card/FeaturedCardCarousel.tsx index d8ccc7c1fb4..60e3a5cc39c 100644 --- a/ts/features/wallet/component/card/FeaturedCardCarousel.tsx +++ b/ts/features/wallet/component/card/FeaturedCardCarousel.tsx @@ -16,7 +16,6 @@ import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import ROUTES from "../../../../navigation/routes"; import { loadServiceDetail, showServiceDetails @@ -38,6 +37,7 @@ import { serviceFromAvailableBonusSelector } from "../../../bonus/common/store/selectors"; import { getRemoteLocale } from "../../../messages/utils/messages"; +import { SERVICES_ROUTES } from "../../../services/navigation/routes"; import FeaturedCard from "./FeaturedCard"; type Props = ReturnType & @@ -105,8 +105,8 @@ const FeaturedCardCarousel: React.FunctionComponent = (props: Props) => { }, s => () => { dispatch(showServiceDetails(s)); - navigation.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICE_DETAIL, + navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICE_DETAIL, params: { serviceId: s.service_id } }); } diff --git a/ts/features/wallet/component/features/PagoPaPaymentCapability.tsx b/ts/features/wallet/component/features/PagoPaPaymentCapability.tsx index 95d59c5cf2d..48305f64520 100644 --- a/ts/features/wallet/component/features/PagoPaPaymentCapability.tsx +++ b/ts/features/wallet/component/features/PagoPaPaymentCapability.tsx @@ -6,7 +6,7 @@ import { PreferencesListItem } from "../../../../components/PreferencesListItem" import TouchableDefaultOpacity from "../../../../components/TouchableDefaultOpacity"; import { IOBadge } from "../../../../components/core/IOBadge"; import { Link } from "../../../../components/core/typography/Link"; -import Markdown from "../../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; import Switch from "../../../../components/ui/Switch"; import I18n from "../../../../i18n"; import { PaymentMethod } from "../../../../types/pagopa"; @@ -69,9 +69,9 @@ const PagoPaPaymentCapability: React.FC = props => { { component: ( - + {I18n.t("wallet.methods.card.pagoPaCapability.bottomSheetBody")} - + ; -type Props = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_CREDIT_CARD_DETAIL" ->; - /** * Detail screen for a credit card */ -const CreditCardDetailScreen = ({ route }: Props) => { +const CreditCardDetailScreen = () => { const [walletExisted, setWalletExisted] = React.useState(false); - const paramCreditCard: CreditCardPaymentMethod = route.params.creditCard; + const { creditCard: paramCreditCard } = + useRoute< + Route<"WALLET_CREDIT_CARD_DETAIL", CreditCardDetailScreenNavigationParams> + >().params; // We need to read the card from the store to receive the updates // TODO: to avoid this we need a store refactoring for the wallet section (all the component should receive the id and not the wallet, in order to update when needed) const storeCreditCard = useIOSelector(state => diff --git a/ts/features/wallet/creditCard/screen/__tests__/CreditCardDetailScreen.test.tsx b/ts/features/wallet/creditCard/screen/__tests__/CreditCardDetailScreen.test.tsx index 58bfeeb6cb9..f7898d87a47 100644 --- a/ts/features/wallet/creditCard/screen/__tests__/CreditCardDetailScreen.test.tsx +++ b/ts/features/wallet/creditCard/screen/__tests__/CreditCardDetailScreen.test.tsx @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import { waitFor } from "@testing-library/react-native"; -import * as React from "react"; import { Store, createStore } from "redux"; import { StatusEnum } from "../../../../../../definitions/idpay/InitiativeDTO"; import { EnableableFunctionsEnum } from "../../../../../../definitions/pagopa/EnableableFunctions"; @@ -166,16 +165,12 @@ describe("Test CreditCardDetailScreen", () => { ); }); -const CreditCardWrapper = ( - props: React.ComponentProps -) => ; - const renderDetailScreen = ( store: Store, creditCard: CreditCardPaymentMethod ) => renderScreenWithNavigationStoreContext( - CreditCardWrapper, + CreditCardDetailScreen, ROUTES.WALLET_CREDIT_CARD_DETAIL, { creditCard }, store diff --git a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts index 195c5d6cffa..60f248c7cdf 100644 --- a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts +++ b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts @@ -8,75 +8,73 @@ describe("Credit Card onboarding", () => { await ensureLoggedIn(); }); - describe("when the user inserts all the required valid data", () => { - it("should add successfully the credit card to the wallet", async () => { - await waitFor(element(by.text(I18n.t("global.navigator.wallet")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + it("when the user inserts all the required valid data, it should add successfully the credit card to the wallet", async () => { + await waitFor(element(by.text(I18n.t("global.navigator.wallet")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // Footer, Wallet icon - await element(by.text(I18n.t("global.navigator.wallet"))).tap(); + // Footer, Wallet icon + await element(by.text(I18n.t("global.navigator.wallet"))).tap(); - await waitFor(element(by.id("walletAddNewPaymentMethodTestId"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + await waitFor(element(by.id("walletAddNewPaymentMethodTestId"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // Button "+ Add" - await element(by.id("walletAddNewPaymentMethodTestId")).tap(); + // Button "+ Add" + await element(by.id("walletAddNewPaymentMethodTestId")).tap(); - await waitFor(element(by.id("wallet.paymentMethod"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + await waitFor(element(by.id("wallet.paymentMethod"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // Add payment method listItem in bottomSheet - await element(by.id("wallet.paymentMethod")).tap(); + // Add payment method listItem in bottomSheet + await element(by.id("wallet.paymentMethod")).tap(); - await waitFor(element(by.text(I18n.t("wallet.methods.card.name")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - // Add Credit Card List Item - await element(by.text(I18n.t("wallet.methods.card.name"))).tap(); + await waitFor(element(by.text(I18n.t("wallet.methods.card.name")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + // Add Credit Card List Item + await element(by.text(I18n.t("wallet.methods.card.name"))).tap(); - await waitFor(element(by.id("cardHolderInput"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + await waitFor(element(by.id("cardHolderInput"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // Fill the credit card data - await element(by.id("cardHolderInput")).typeText("Gian Maria Mario"); + // Fill the credit card data + await element(by.id("cardHolderInput")).typeText("Gian Maria Mario"); - await element(by.id("panInputMask")).typeText("4444333322221111"); + await element(by.id("panInputMask")).typeText("4444333322221111"); - await element(by.id("expirationDateInputMask")).typeText("1299"); + await element(by.id("expirationDateInputMask")).typeText("1299"); - await element(by.id("securityCodeInputMask")).typeText("123"); + await element(by.id("securityCodeInputMask")).typeText("123"); - // Close the keyboard - await closeKeyboard(); - await element(by.text(I18n.t("global.buttons.continue"))).tap(); + // Close the keyboard + await closeKeyboard(); + await element(by.text(I18n.t("global.buttons.continue"))).tap(); - await waitFor(element(by.id("saveOrContinueButton"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - await element(by.id("saveOrContinueButton")).tap(); + await waitFor(element(by.id("saveOrContinueButton"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + await element(by.id("saveOrContinueButton")).tap(); - // Wait for 3ds webview - await waitFor(element(by.text(I18n.t("wallet.challenge3ds.description")))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); + // Wait for 3ds webview + await waitFor(element(by.text(I18n.t("wallet.challenge3ds.description")))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); - // Wait for success screen - await waitFor( - element( - by.text(I18n.t("wallet.outcomeMessage.addCreditCard.success.title")) - ) + // Wait for success screen + await waitFor( + element( + by.text(I18n.t("wallet.outcomeMessage.addCreditCard.success.title")) ) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - - // Wait for return to wallet - await waitFor(element(by.id("wallet-home-header-title"))) - .toBeVisible() - .withTimeout(e2eWaitRenderTimeout); - }); + ) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); + + // Wait for return to wallet + await waitFor(element(by.id("wallet-home-header-title"))) + .toBeVisible() + .withTimeout(e2eWaitRenderTimeout); }); }); diff --git a/ts/features/wallet/onboarding/bancomatPay/analytics/index.ts b/ts/features/wallet/onboarding/bancomatPay/analytics/index.ts index 02d709bae49..67f2349b089 100644 --- a/ts/features/wallet/onboarding/bancomatPay/analytics/index.ts +++ b/ts/features/wallet/onboarding/bancomatPay/analytics/index.ts @@ -17,7 +17,7 @@ import { export const trackBPayAction = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(walletAddBPayStart): case getType(walletAddBPayCompleted): @@ -52,5 +52,4 @@ export const trackBPayAction = reason: action.payload }); } - return Promise.resolve(); }; diff --git a/ts/features/wallet/onboarding/bancomatPay/navigation/navigator.tsx b/ts/features/wallet/onboarding/bancomatPay/navigation/navigator.tsx index cb1353d0cbc..5992ae302db 100644 --- a/ts/features/wallet/onboarding/bancomatPay/navigation/navigator.tsx +++ b/ts/features/wallet/onboarding/bancomatPay/navigation/navigator.tsx @@ -12,8 +12,7 @@ const Stack = createStackNavigator(); const PaymentMethodOnboardingBPayNavigator = () => ( ) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(walletAddCoBadgeStart): return mp.track(action.type, { @@ -55,7 +55,6 @@ export const trackCoBadgeAction = case getType(sendAddCobadgeMessage): return mp.track(action.type, { canAdd: action.payload }); } - return Promise.resolve(); }; /** diff --git a/ts/features/wallet/onboarding/cobadge/navigation/navigator.tsx b/ts/features/wallet/onboarding/cobadge/navigation/navigator.tsx index 32ac532a1a0..7777e13c577 100644 --- a/ts/features/wallet/onboarding/cobadge/navigation/navigator.tsx +++ b/ts/features/wallet/onboarding/cobadge/navigation/navigator.tsx @@ -12,8 +12,7 @@ const Stack = createStackNavigator(); const PaymentMethodOnboardingCoBadgeNavigator = () => ( ) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(walletAddPaypalFailure): case getType(walletAddPaypalCancel): @@ -49,7 +49,6 @@ const trackPaypalOnboarding = reason: getNetworkErrorMessage(action.payload) }); } - return Promise.resolve(); }; export default trackPaypalOnboarding; diff --git a/ts/features/wallet/onboarding/paypal/navigation/navigator.tsx b/ts/features/wallet/onboarding/paypal/navigation/navigator.tsx index d27f50db30a..8c4c07a231d 100644 --- a/ts/features/wallet/onboarding/paypal/navigation/navigator.tsx +++ b/ts/features/wallet/onboarding/paypal/navigation/navigator.tsx @@ -13,8 +13,7 @@ const Stack = createStackNavigator(); export const PaymentMethodOnboardingPayPalOnboardingNavigator = () => ( & ReturnType; @@ -104,7 +105,7 @@ const CheckoutContent = ( * 4. navigate to the checkout completed screen */ const PayPalOnboardingCheckoutScreen = (props: Props) => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const { refreshPMtoken } = props; // refresh the PM at the startup useEffect(() => { @@ -113,8 +114,11 @@ const PayPalOnboardingCheckoutScreen = (props: Props) => { const handleCheckoutCompleted = (outcomeCode: O.Option) => { props.setOutcomeCode(outcomeCode); - navigation.navigate(PAYPAL_ROUTES.ONBOARDING.MAIN, { - screen: PAYPAL_ROUTES.ONBOARDING.CHECKOUT_COMPLETED + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: PAYPAL_ROUTES.ONBOARDING.MAIN, + params: { + screen: PAYPAL_ROUTES.ONBOARDING.CHECKOUT_COMPLETED + } }); }; diff --git a/ts/features/wallet/onboarding/paypal/screen/PayPalPspSelectionScreen.tsx b/ts/features/wallet/onboarding/paypal/screen/PayPalPspSelectionScreen.tsx index 3973cc68a16..b63938e9d40 100644 --- a/ts/features/wallet/onboarding/paypal/screen/PayPalPspSelectionScreen.tsx +++ b/ts/features/wallet/onboarding/paypal/screen/PayPalPspSelectionScreen.tsx @@ -1,4 +1,3 @@ -import { useNavigation } from "@react-navigation/native"; import React, { useEffect, useState } from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import { connect, useDispatch } from "react-redux"; @@ -35,6 +34,8 @@ import { import { payPalPspSelector } from "../store/reducers/searchPsp"; import { IOPayPalPsp } from "../types"; import PAYPAL_ROUTES from "../navigation/routes"; +import { useIONavigation } from "../../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../../navigation/routes"; type Props = ReturnType & ReturnType; @@ -112,7 +113,7 @@ const PayPalPspSelectionScreen = (props: Props): React.ReactElement | null => { const pspList = getValueOrElse(props.pspList, []); const [selectedPsp, setSelectedPsp] = useState(); const dispatch = useDispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const searchPaypalPsp = () => { dispatch(searchPaypalPspAction.request()); }; @@ -136,8 +137,11 @@ const PayPalPspSelectionScreen = (props: Props): React.ReactElement | null => { onPress: () => { if (selectedPsp) { props.setPspSelected(selectedPsp); - navigation.navigate(PAYPAL_ROUTES.ONBOARDING.MAIN, { - screen: PAYPAL_ROUTES.ONBOARDING.CHECKOUT + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: PAYPAL_ROUTES.ONBOARDING.MAIN, + params: { + screen: PAYPAL_ROUTES.ONBOARDING.CHECKOUT + } }); } }, diff --git a/ts/features/wallet/onboarding/paypal/screen/PayPalStartOnboardingScreen.tsx b/ts/features/wallet/onboarding/paypal/screen/PayPalStartOnboardingScreen.tsx index cc2d267bc92..163db8d270f 100644 --- a/ts/features/wallet/onboarding/paypal/screen/PayPalStartOnboardingScreen.tsx +++ b/ts/features/wallet/onboarding/paypal/screen/PayPalStartOnboardingScreen.tsx @@ -1,4 +1,3 @@ -import { useNavigation } from "@react-navigation/native"; import React from "react"; import { Dimensions, SafeAreaView, View } from "react-native"; import { connect } from "react-redux"; @@ -16,6 +15,8 @@ import { GlobalState } from "../../../../../store/reducers/types"; import { emptyContextualHelp } from "../../../../../utils/emptyContextualHelp"; import { walletAddPaypalBack, walletAddPaypalCancel } from "../store/actions"; import PAYPAL_ROUTES from "../navigation/routes"; +import { useIONavigation } from "../../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../../navigation/routes"; type Props = ReturnType & ReturnType; @@ -50,11 +51,14 @@ const PayPalLogo = () => ( const PayPalStartOnboardingScreen = ( props: Props ): React.ReactElement | null => { - const navigationContext = useNavigation(); + const navigationContext = useIONavigation(); const navigateToSearchPsp = () => - navigationContext.navigate(PAYPAL_ROUTES.ONBOARDING.MAIN, { - screen: PAYPAL_ROUTES.ONBOARDING.SEARCH_PSP + navigationContext.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: PAYPAL_ROUTES.ONBOARDING.MAIN, + params: { + screen: PAYPAL_ROUTES.ONBOARDING.SEARCH_PSP + } }); const cancelButtonProps = { diff --git a/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PayPalOnboardingCompletedSuccessComponent.test.tsx.snap b/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PayPalOnboardingCompletedSuccessComponent.test.tsx.snap index 4b0346d338d..bd5b030390c 100644 --- a/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PayPalOnboardingCompletedSuccessComponent.test.tsx.snap +++ b/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PayPalOnboardingCompletedSuccessComponent.test.tsx.snap @@ -20,502 +20,541 @@ exports[`PayPalOnboardingCompletedSuccessComponent should match the snapshot 1`] } > - - - + + /> + + - + - N/A - + + N/A + + + - - - - - + - - - + + + - Fatto! - - - + Fatto! + + + - Ora puoi continuare con il pagamento. - - - + } + } + style={ + Array [ + Object { + "textAlign": "center", + }, + Object { + "fontSize": 16, + "lineHeight": 24, + }, + Object { + "color": "#475A6D", + "fontFamily": "Titillium Web", + "fontStyle": "normal", + "fontWeight": "400", + }, + ] + } + testID="infoScreenBody" + weight="Regular" + > + Ora puoi continuare con il pagamento. + + - - Continua - + + Continua + + @@ -525,8 +564,8 @@ exports[`PayPalOnboardingCompletedSuccessComponent should match the snapshot 1`] - - + + diff --git a/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PspRadioItem.test.tsx.snap b/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PspRadioItem.test.tsx.snap index d4e689fcca3..6a34c5ff3dd 100644 --- a/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PspRadioItem.test.tsx.snap +++ b/ts/features/wallet/onboarding/paypal/screen/__tests__/__snapshots__/PspRadioItem.test.tsx.snap @@ -73,7 +73,6 @@ exports[`PspRadioItem match snapshot 1`] = ` align="xMidYMid" bbHeight={24} bbWidth={24} - color={4278219750} focusable={false} height={24} importantForAccessibility="no-hide-descendants" @@ -96,19 +95,26 @@ exports[`PspRadioItem match snapshot 1`] = ` }, ] } - tintColor={4278219750} + tintColor="#0073E6" vbHeight={24} vbWidth={24} width={24} > - + - + - + - + ; /** * This screen is where the user updates the PSP that will be used for the payment * Only 1 psp can be selected */ -const PayPalPspUpdateScreen: React.FunctionComponent = ( - props: Props -) => { +const PayPalPspUpdateScreen: React.FunctionComponent = () => { + const { idPayment, idWallet } = + useRoute< + Route< + "WALLET_PAYPAL_UPDATE_PAYMENT_PSP", + PayPalPspUpdateScreenNavigationParams + > + >().params; const locales = getLocales(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const dispatch = useDispatch(); const pspList = useIOSelector(pspV2ListSelector); - const idPayment = props.route.params.idPayment; - const idWallet = props.route.params.idWallet; const searchPaypalPsp = () => { dispatch(pspForPaymentV2.request({ idPayment, idWallet })); }; - useEffect(searchPaypalPsp, [dispatch]); + useEffect(searchPaypalPsp, [dispatch, idPayment, idWallet]); const goBack = () => navigation.goBack(); return ( diff --git a/ts/features/wallet/paypal/screen/PaypalDetailScreen.tsx b/ts/features/wallet/paypal/screen/PaypalDetailScreen.tsx index 583e6abd855..8389e2aaa99 100644 --- a/ts/features/wallet/paypal/screen/PaypalDetailScreen.tsx +++ b/ts/features/wallet/paypal/screen/PaypalDetailScreen.tsx @@ -3,15 +3,15 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; -import { - PaymentCardBig, - PaymentCardBigProps -} from "../../../../components/ui/cards/payment/PaymentCardBig"; import { useIOSelector } from "../../../../store/hooks"; import { paypalSelector } from "../../../../store/reducers/wallet/wallets"; import { getPaypalAccountEmail } from "../../../../utils/paypal"; import BasePaymentMethodScreen from "../../common/BasePaymentMethodScreen"; import PaymentMethodFeatures from "../../component/features/PaymentMethodFeatures"; +import { + PaymentCardBig, + PaymentCardBigProps +} from "../../../payments/common/components/PaymentCardBig"; /** * Detail screen for a PayPal payment method diff --git a/ts/features/walletV3/barcode/navigation/navigator.tsx b/ts/features/walletV3/barcode/navigation/navigator.tsx deleted file mode 100644 index fd594ab997e..00000000000 --- a/ts/features/walletV3/barcode/navigation/navigator.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ParamListBase } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp, - TransitionPresets -} from "@react-navigation/stack"; -import React from "react"; -import { isGestureEnabled } from "../../../../utils/navigation"; -import { WalletBarcodeChoiceScreen } from "../screens/WalletBarcodeChoiceScreen"; -import { WalletBarcodeScanScreen } from "../screens/WalletBarcodeScanScreen"; -import { WalletBarcodeParamsList } from "./params"; -import { WalletBarcodeRoutes } from "./routes"; - -const Stack = createStackNavigator(); - -export const WalletBarcodeNavigator = () => ( - - - - -); - -export type WalletBarcodeStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; - -export type WalletBarcodeStackNavigation = WalletBarcodeStackNavigationProp< - WalletBarcodeParamsList, - keyof WalletBarcodeParamsList ->; diff --git a/ts/features/walletV3/barcode/navigation/params.ts b/ts/features/walletV3/barcode/navigation/params.ts deleted file mode 100644 index 11b34aede6a..00000000000 --- a/ts/features/walletV3/barcode/navigation/params.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { WalletBarcodeChoiceScreenParams } from "../screens/WalletBarcodeChoiceScreen"; -import { WalletBarcodeRoutes } from "./routes"; - -export type WalletBarcodeParamsList = { - [WalletBarcodeRoutes.WALLET_BARCODE_MAIN]: undefined; - [WalletBarcodeRoutes.WALLET_BARCODE_SCAN]: undefined; - [WalletBarcodeRoutes.WALLET_BARCODE_CHOICE]: WalletBarcodeChoiceScreenParams; -}; diff --git a/ts/features/walletV3/barcode/navigation/routes.ts b/ts/features/walletV3/barcode/navigation/routes.ts deleted file mode 100644 index 1a798293608..00000000000 --- a/ts/features/walletV3/barcode/navigation/routes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const WalletBarcodeRoutes = { - WALLET_BARCODE_MAIN: "WALLET_BARCODE_MAIN", - WALLET_BARCODE_SCAN: "WALLET_BARCODE_SCAN", - WALLET_BARCODE_CHOICE: "WALLET_BARCODE_CHOICE" -} as const; diff --git a/ts/features/walletV3/common/api/client.ts b/ts/features/walletV3/common/api/client.ts deleted file mode 100644 index a17ce98e752..00000000000 --- a/ts/features/walletV3/common/api/client.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createClient } from "../../../../../definitions/pagopa/walletv3/client"; -import { defaultRetryingFetch } from "../../../../utils/fetch"; - -const createWalletClient = (baseUrl: string, bearerAuth: string) => - createClient<"bearerAuth">({ - baseUrl, - basePath: "/payment-wallet/v1", - fetchApi: defaultRetryingFetch(), - withDefaults: op => params => { - const paramsWithDefaults = { - ...params, - bearerAuth - } as Parameters[0]; - - return op(paramsWithDefaults); - } - }); - -export type WalletClient = ReturnType; - -export { createWalletClient }; diff --git a/ts/features/walletV3/common/saga/index.ts b/ts/features/walletV3/common/saga/index.ts deleted file mode 100644 index cc9f9eb76a9..00000000000 --- a/ts/features/walletV3/common/saga/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SagaIterator } from "redux-saga"; -import { fork, select } from "typed-redux-saga/macro"; -import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; -import { watchWalletOnboardingSaga } from "../../onboarding/saga"; -import { createPaymentClient } from "../../payment/api/client"; -import { watchWalletPaymentSaga } from "../../payment/saga"; -import { createWalletClient } from "../api/client"; -import { walletApiBaseUrl, walletApiUatBaseUrl } from "../../../../config"; -import { watchWalletDetailsSaga } from "../../details/saga"; -import { watchWalletTransactionSaga } from "../../transaction/saga"; - -export function* watchWalletSaga(walletToken: string): SagaIterator { - const isPagoPATestEnabled = yield* select(isPagoPATestEnabledSelector); - - const walletBaseUrl = isPagoPATestEnabled - ? walletApiUatBaseUrl - : walletApiBaseUrl; - - const walletClient = createWalletClient(walletBaseUrl, walletToken); - const paymentClient = createPaymentClient(walletBaseUrl, walletToken); - - yield* fork(watchWalletOnboardingSaga, walletClient); - yield* fork(watchWalletPaymentSaga, walletClient, paymentClient); - yield* fork(watchWalletDetailsSaga, walletClient); - yield* fork(watchWalletTransactionSaga, walletClient); -} diff --git a/ts/features/walletV3/common/store/actions/index.ts b/ts/features/walletV3/common/store/actions/index.ts deleted file mode 100644 index fde34b55063..00000000000 --- a/ts/features/walletV3/common/store/actions/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { WalletDetailsActions } from "../../../details/store/actions"; -import { WalletOnboardingActions } from "../../../onboarding/store/actions"; -import { WalletPaymentActions } from "../../../payment/store/actions"; -import { WalletTransactionActions } from "../../../transaction/store/actions"; - -export type WalletActions = - | WalletOnboardingActions - | WalletDetailsActions - | WalletPaymentActions - | WalletTransactionActions; diff --git a/ts/features/walletV3/common/store/reducers/index.ts b/ts/features/walletV3/common/store/reducers/index.ts deleted file mode 100644 index 071b27a0448..00000000000 --- a/ts/features/walletV3/common/store/reducers/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { combineReducers } from "redux"; -import walletOnboardingReducer, { - WalletOnboardingState -} from "../../../onboarding/store"; -import walletPaymentReducer, { - WalletPaymentState -} from "../../../payment/store/reducers"; -import walletDetailsReducer, { - WalletDetailsState -} from "../../../details/store"; -import walletTransactionReducer, { - WalletTransactionState -} from "../../../transaction/store"; - -export type WalletState = { - onboarding: WalletOnboardingState; - details: WalletDetailsState; - payment: WalletPaymentState; - transaction: WalletTransactionState; -}; - -const walletReducer = combineReducers({ - onboarding: walletOnboardingReducer, - details: walletDetailsReducer, - payment: walletPaymentReducer, - transaction: walletTransactionReducer -}); - -export default walletReducer; diff --git a/ts/features/walletV3/details/components/WalletDetailsPaymentMethodScreen.tsx b/ts/features/walletV3/details/components/WalletDetailsPaymentMethodScreen.tsx deleted file mode 100644 index 626b16227f2..00000000000 --- a/ts/features/walletV3/details/components/WalletDetailsPaymentMethodScreen.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useNavigation } from "@react-navigation/native"; -import * as React from "react"; -import { - Alert, - GestureResponderEvent, - Platform, - StyleSheet, - View -} from "react-native"; -import { ScrollView } from "react-native-gesture-handler"; -import { - IOColors, - VSpacer, - IOSpacingScale, - ListItemAction, - IOStyles -} from "@pagopa/io-app-design-system"; -import I18n from "../../../../i18n"; -import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { IOToast } from "../../../../components/Toast"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; -import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { isDesignSystemEnabledSelector } from "../../../../store/reducers/persistedPreferences"; -import { walletDetailsInstrumentPotSelector } from "../store"; -import { walletDetailsDeleteInstrument } from "../store/actions"; - -type Props = { - paymentMethod?: WalletInfo; - card: React.ReactNode; - content: React.ReactNode; - headerTitle?: string; -}; - -// ----------------------------- component ----------------------------------- - -/** - * Base layout for payment methods screen & legacy delete handling - */ -const WalletDetailsPaymentMethodScreen = (props: Props) => { - const { card, content, paymentMethod } = props; - const hasErrorDelete = pot.isError( - useIOSelector(walletDetailsInstrumentPotSelector) - ); - const [isLoadingDelete, setIsLoadingDelete] = React.useState(false); - const dispatch = useIODispatch(); - const isDSenabled = useIOSelector(isDesignSystemEnabledSelector); - const blueHeaderColor = isDSenabled ? IOColors["blueIO-600"] : IOColors.blue; - - const navigation = useNavigation(); - - const deleteWallet = (walletId: string) => - dispatch( - walletDetailsDeleteInstrument.request({ - walletId, - onSuccess: _ => { - IOToast.success(I18n.t("wallet.delete.successful")); - navigation.goBack(); - }, - onFailure: _ => { - IOToast.error(I18n.t("wallet.delete.failed")); - } - }) - ); - - React.useEffect(() => { - if (hasErrorDelete) { - setIsLoadingDelete(false); - } - }, [hasErrorDelete]); - - const onDeleteMethod = () => { - // Create a native Alert to confirm or cancel the delete action - Alert.alert( - I18n.t("wallet.newRemove.title"), - I18n.t("wallet.newRemove.body"), - [ - { - text: - Platform.OS === "ios" - ? I18n.t(`wallet.delete.ios.confirm`) - : I18n.t(`wallet.delete.android.confirm`), - style: "destructive", - onPress: () => { - if (paymentMethod) { - deleteWallet(paymentMethod.walletId); - } - } - }, - { - text: I18n.t("global.buttons.cancel"), - style: "default" - } - ], - { cancelable: false } - ); - }; - - if (isLoadingDelete) { - return ( - - ); - } - - return ( - - - - - {card} - - - - {content} - - {paymentMethod && } - - - - - ); -}; - -// ----------------------------- utils ----------------------------------- - -const DeleteButton = ({ - onPress -}: { - onPress: (event: GestureResponderEvent) => void; -}) => ( - -); - -// ----------------------------- styles ----------------------------------- - -const cardContainerHorizontalSpacing: IOSpacingScale = 16; - -const styles = StyleSheet.create({ - cardContainer: { - paddingHorizontal: cardContainerHorizontalSpacing, - alignItems: "center", - marginBottom: "-15%", - aspectRatio: 1.7, - width: "100%" - }, - blueHeader: { - paddingTop: "75%", - marginTop: "-75%", - marginBottom: "15%" - } -}); - -export default WalletDetailsPaymentMethodScreen; diff --git a/ts/features/walletV3/details/navigation/navigator.tsx b/ts/features/walletV3/details/navigation/navigator.tsx deleted file mode 100644 index 49408d21d44..00000000000 --- a/ts/features/walletV3/details/navigation/navigator.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ParamListBase } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp -} from "@react-navigation/stack"; -import React from "react"; -import { isGestureEnabled } from "../../../../utils/navigation"; -import WalletDetailsScreen, { - WalletDetailsScreenNavigationParams -} from "../screens/WalletDetailsScreen"; - -export const WalletDetailsRoutes = { - WALLET_DETAILS_MAIN: "WALLET_DETAILS_MAIN", - WALLET_DETAILS_SCREEN: "WALLET_DETAILS_SCREEN" -} as const; - -export type WalletDetailsParamsList = { - [WalletDetailsRoutes.WALLET_DETAILS_MAIN]: undefined; - [WalletDetailsRoutes.WALLET_DETAILS_SCREEN]: WalletDetailsScreenNavigationParams; -}; - -const Stack = createStackNavigator(); - -export const WalletDetailsNavigator = () => ( - - - -); - -export type WalletDetailsStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; - -export type WalletDetailsStackNavigation = WalletDetailsStackNavigationProp< - WalletDetailsParamsList, - keyof WalletDetailsParamsList ->; diff --git a/ts/features/walletV3/details/saga/handleTogglePagoPaCapability.ts b/ts/features/walletV3/details/saga/handleTogglePagoPaCapability.ts deleted file mode 100644 index c64a9f66823..00000000000 --- a/ts/features/walletV3/details/saga/handleTogglePagoPaCapability.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { call, put, select } from "typed-redux-saga/macro"; -import * as E from "fp-ts/lib/Either"; -import { ActionType } from "typesafe-actions"; -import { SagaCallReturnType } from "../../../../types/utils"; -import { - walletDetailsGetInstrument, - walletDetailsPagoPaCapabilityToggle -} from "../store/actions"; -import { readablePrivacyReport } from "../../../../utils/reporters"; -import { getGenericError, getNetworkError } from "../../../../utils/errors"; -import { WalletClient } from "../../common/api/client"; -import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; -import { walletDetailsInstrumentSelector } from "../store"; -import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; -import { Service } from "../../../../../definitions/pagopa/walletv3/Service"; -import { ServiceStatusEnum } from "../../../../../definitions/pagopa/walletv3/ServiceStatus"; -import { WalletService } from "../../../../../definitions/pagopa/walletv3/WalletService"; - -/** - * Handle the remote call to toggle the Wallet pagopa capability - */ -export function* handleTogglePagoPaCapability( - updateWalletServicesById: WalletClient["updateWalletServicesById"], - action: ActionType<(typeof walletDetailsPagoPaCapabilityToggle)["request"]> -) { - try { - const walletDetails = yield* select(walletDetailsInstrumentSelector); - if (!walletDetails) { - throw new Error("walletDetails is undefined"); - } - const updatedServices = walletDetails.services.map(service => ({ - ...service, - status: updatePagoPaServiceStatus(service) - })); - - const updateWalletPagoPaServicesRequest = updateWalletServicesById({ - walletId: action.payload.walletId, - body: { - services: updatedServices as Array - } - }); - const updateWalletResult = (yield* call( - withRefreshApiCall, - updateWalletPagoPaServicesRequest, - action - )) as unknown as SagaCallReturnType; - if (E.isRight(updateWalletResult)) { - if (updateWalletResult.right.status === 204) { - // handled success - const successAction = walletDetailsPagoPaCapabilityToggle.success(); - yield* put(successAction); - if (action.payload.onSuccess) { - action.payload.onSuccess(successAction); - } - return; - } - // not handled error codes - const failureAction = walletDetailsPagoPaCapabilityToggle.failure({ - ...getGenericError( - new Error(`response status code ${updateWalletResult.right.status}`) - ) - }); - yield* put(failureAction); - if (action.payload.onFailure) { - action.payload.onFailure(failureAction); - } - } else { - // cannot decode response - const failureAction = walletDetailsPagoPaCapabilityToggle.failure({ - ...getGenericError( - new Error(readablePrivacyReport(updateWalletResult.left)) - ) - }); - yield* put(failureAction); - if (action.payload.onFailure) { - action.payload.onFailure(failureAction); - } - } - } catch (e) { - yield* put(walletDetailsGetInstrument.failure({ ...getNetworkError(e) })); - } -} - -const updatePagoPaServiceStatus = ( - service: Service -): ServiceStatusEnum | undefined => { - if (service.name === ServiceNameEnum.PAGOPA) { - return service.status === ServiceStatusEnum.DISABLED - ? ServiceStatusEnum.ENABLED - : ServiceStatusEnum.DISABLED; - } - return service.status; -}; diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx deleted file mode 100644 index d371306e44b..00000000000 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import * as React from "react"; -import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; -import { useDispatch } from "react-redux"; -import { IOLogoPaymentExtType } from "@pagopa/io-app-design-system"; - -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; -import { PaymentCardBig } from "../../../../components/ui/cards/payment/PaymentCardBig"; -import { useIOSelector } from "../../../../store/hooks"; -import { idPayAreInitiativesFromInstrumentLoadingSelector } from "../../../idpay/wallet/store/reducers"; -import { capitalize } from "../../../../utils/strings"; -import WalletDetailsPaymentMethodScreen from "../components/WalletDetailsPaymentMethodScreen"; -import WalletDetailsPaymentMethodFeatures from "../../common/components/WalletDetailsPaymentMethodFeatures"; -import { WalletDetailsParamsList } from "../navigation/navigator"; -import { - isErrorWalletInstrumentSelector, - isLoadingWalletInstrumentSelector, - walletDetailsInstrumentSelector -} from "../store"; -import { walletDetailsGetInstrument } from "../store/actions"; -import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; -import { getDateFromExpiryDate } from "../../../../utils/dates"; -import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; -import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; - -export type WalletDetailsScreenNavigationParams = Readonly<{ - walletId: string; -}>; - -export type WalletDetailsScreenRouteProps = RouteProp< - WalletDetailsParamsList, - "WALLET_DETAILS_SCREEN" ->; - -const generateCardComponent = (details: UIWalletInfoDetails) => { - if (details.maskedEmail !== undefined) { - return ( - - ); - } - - return ( - - ); -}; - -const generateCardHeaderTitle = (details?: UIWalletInfoDetails) => { - if (details?.maskedPan !== undefined) { - const capitalizedCardCircuit = capitalize( - details.brand?.toLowerCase() ?? "" - ); - return `${capitalizedCardCircuit} ••${details.maskedPan}`; - } - - return ""; -}; - -/** - * Detail screen for a credit card - */ -const WalletDetailsScreen = () => { - const route = useRoute(); - const navigation = useNavigation>(); - const dispatch = useDispatch(); - const { walletId } = route.params; - const walletDetails = useIOSelector(walletDetailsInstrumentSelector); - const isLoadingWalletDetails = useIOSelector( - isLoadingWalletInstrumentSelector - ); - const isErrorWalletDetails = useIOSelector(isErrorWalletInstrumentSelector); - const areIdpayInitiativesLoading = useIOSelector( - idPayAreInitiativesFromInstrumentLoadingSelector - ); - - const WalletDetailsGenericFailure = () => ( - navigation.pop() - }} - secondaryAction={{ - label: I18n.t("wallet.methodDetails.error.secondaryButton"), - accessibilityLabel: I18n.t( - "wallet.methodDetails.error.secondaryButton" - ), - onPress: handleOnRetry - }} - /> - ); - - const handleOnRetry = () => { - dispatch(walletDetailsGetInstrument.request({ walletId })); - }; - - React.useEffect(() => { - dispatch(walletDetailsGetInstrument.request({ walletId })); - }, [walletId, dispatch]); - - if (isLoadingWalletDetails) { - return ( - } - content={<>} - /> - ); - } - - if (walletDetails !== undefined) { - const cardComponent = pipe( - walletDetails.details, - O.fromNullable, - O.fold( - () => , - details => generateCardComponent(details) - ) - ); - - return ( - - - } - headerTitle={generateCardHeaderTitle(walletDetails.details)} - /> - - ); - } else if (isErrorWalletDetails) { - return ; - } - return null; -}; - -export default WalletDetailsScreen; diff --git a/ts/features/walletV3/details/store/actions/index.ts b/ts/features/walletV3/details/store/actions/index.ts deleted file mode 100644 index 5396b3a70ce..00000000000 --- a/ts/features/walletV3/details/store/actions/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ActionType, createAsyncAction } from "typesafe-actions"; -import { NetworkError } from "../../../../../utils/errors"; -import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; - -export type WalletDetailsGetInstrumentPayload = { - walletId: string; -}; - -export type WalletDetailsDeleteinstrumentPayload = { - walletId: string; - onSuccess?: ( - action: ActionType - ) => void; - onFailure?: ( - action: ActionType - ) => void; -}; - -export const walletDetailsGetInstrument = createAsyncAction( - "WALLET_DETAILS_GET_INSTRUMENT_REQUEST", - "WALLET_DETAILS_GET_INSTRUMENT_SUCCESS", - "WALLET_DETAILS_GET_INSTRUMENT_FAILURE", - "WALLET_DETAILS_GET_INSTRUMENT_CANCEL" -)(); - -export const walletDetailsDeleteInstrument = createAsyncAction( - "WALLET_DETAILS_DELETE_INSTRUMENT_REQUEST", - "WALLET_DETAILS_DELETE_INSTRUMENT_SUCCESS", - "WALLET_DETAILS_DELETE_INSTRUMENT_FAILURE", - "WALLET_DETAILS_DELETE_INSTRUMENT_CANCEL" -)(); - -export type WalletDetailsPagoPaCapabilityTogglePayload = { - walletId: string; - onSuccess?: ( - action: ActionType - ) => void; - onFailure?: ( - action: ActionType - ) => void; -}; - -export const walletDetailsPagoPaCapabilityToggle = createAsyncAction( - "WALLET_DETAILS_PAGOPA_CAPABILITY_TOGGLE_REQUEST", - "WALLET_DETAILS_PAGOPA_CAPABILITY_TOGGLE_SUCCESS", - "WALLET_DETAILS_PAGOPA_CAPABILITY_TOGGLE_FAILURE", - "WALLET_DETAILS_PAGOPA_CAPABILITY_TOGGLE_CANCEL" -)(); - -export type WalletDetailsActions = - | ActionType - | ActionType - | ActionType; diff --git a/ts/features/walletV3/details/store/index.ts b/ts/features/walletV3/details/store/index.ts deleted file mode 100644 index abdb79fae02..00000000000 --- a/ts/features/walletV3/details/store/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as _ from "lodash"; -import { createSelector } from "reselect"; -import { getType } from "typesafe-actions"; -import { Action } from "../../../../store/actions/types"; -import { NetworkError } from "../../../../utils/errors"; -import { GlobalState } from "../../../../store/reducers/types"; - -import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { ServiceStatusEnum } from "../../../../../definitions/pagopa/walletv3/ServiceStatus"; -import { - walletDetailsGetInstrument, - walletDetailsPagoPaCapabilityToggle -} from "./actions"; - -export type WalletDetailsState = { - walletDetails: pot.Pot; -}; - -const INITIAL_STATE: WalletDetailsState = { - walletDetails: pot.noneLoading -}; - -const walletDetailsReducer = ( - state: WalletDetailsState = INITIAL_STATE, - action: Action -): WalletDetailsState => { - switch (action.type) { - // GET WALLET DETAILS - case getType(walletDetailsGetInstrument.request): - return { - ...state, - walletDetails: pot.toLoading(pot.none) - }; - case getType(walletDetailsGetInstrument.success): - return { - ...state, - walletDetails: pot.some(action.payload) - }; - case getType(walletDetailsGetInstrument.failure): - return { - ...state, - walletDetails: pot.toError(state.walletDetails, action.payload) - }; - case getType(walletDetailsGetInstrument.cancel): - return { - ...state, - walletDetails: pot.none - }; - // TOGGLE PAGOPA CAPABILITY - case getType(walletDetailsPagoPaCapabilityToggle.success): - const walletDetails = pot.getOrElse( - state.walletDetails, - {} as WalletInfo - ); - const updatedServices = walletDetails.services.map(service => { - if (service.name === ServiceNameEnum.PAGOPA) { - return { - ...service, - status: - service.status === ServiceStatusEnum.ENABLED - ? ServiceStatusEnum.DISABLED - : ServiceStatusEnum.ENABLED - }; - } - return service; - }); - return { - ...state, - walletDetails: pot.some({ - ...walletDetails, - services: updatedServices - }) - }; - } - return state; -}; - -const walletDetailsSelector = (state: GlobalState) => - state.features.wallet.details; - -export const walletDetailsInstrumentPotSelector = createSelector( - walletDetailsSelector, - details => details.walletDetails -); - -export const walletDetailsInstrumentSelector = createSelector( - walletDetailsInstrumentPotSelector, - details => pot.toUndefined(details) -); - -export const isLoadingWalletInstrumentSelector = createSelector( - walletDetailsInstrumentPotSelector, - walletInstrument => pot.isLoading(walletInstrument) -); - -export const isErrorWalletInstrumentSelector = createSelector( - walletDetailsInstrumentPotSelector, - walletInstrument => pot.isError(walletInstrument) -); - -export default walletDetailsReducer; diff --git a/ts/features/walletV3/onboarding/components/WalletOnboardingError.tsx b/ts/features/walletV3/onboarding/components/WalletOnboardingError.tsx deleted file mode 100644 index 5c2af8dd381..00000000000 --- a/ts/features/walletV3/onboarding/components/WalletOnboardingError.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable functional/immutable-data */ -import * as React from "react"; - -import { OnboardingOutcomeEnum, OnboardingOutcomeFailure } from "../types"; -import I18n from "../../../../i18n"; -import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; -import { openWebUrl } from "../../../../utils/url"; -import { - ONBOARDING_FAQ_ENABLE_3DS, - ONBOARDING_OUTCOME_ERROR_PICTOGRAM -} from "../utils"; - -type WalletOnboardingErrorProps = { - onClose: () => void; - outcome: OnboardingOutcomeFailure; -}; - -type OnboardingErrorEnumKey = Exclude< - keyof typeof OnboardingOutcomeEnum, - "SUCCESS" ->; - -/** - * Component used to show an error message when the wallet onboarding fails with a specific outcome - */ -const WalletOnboardingError = ({ - onClose, - outcome -}: WalletOnboardingErrorProps) => { - const handleOnPressPrimaryAction = () => { - onClose(); - }; - const handleOnPressSecondaryAction = () => { - switch (outcome) { - case OnboardingOutcomeEnum.AUTH_ERROR: - openWebUrl(ONBOARDING_FAQ_ENABLE_3DS); - } - }; - - const outcomeEnumKey = Object.keys(OnboardingOutcomeEnum)[ - Object.values(OnboardingOutcomeEnum).indexOf( - outcome as OnboardingOutcomeEnum - ) - ] as OnboardingErrorEnumKey; - - const renderSecondaryAction = () => { - switch (outcome) { - case OnboardingOutcomeEnum.AUTH_ERROR: - return { - label: I18n.t(`wallet.onboarding.failure.AUTH_ERROR.secondaryAction`), - accessibilityLabel: I18n.t( - `wallet.onboarding.failure.AUTH_ERROR.secondaryAction` - ), - onPress: handleOnPressSecondaryAction - }; - } - return undefined; - }; - - return ( - - ); -}; - -export default WalletOnboardingError; diff --git a/ts/features/walletV3/onboarding/components/WalletOnboardingSuccess.tsx b/ts/features/walletV3/onboarding/components/WalletOnboardingSuccess.tsx deleted file mode 100644 index 299c36832bf..00000000000 --- a/ts/features/walletV3/onboarding/components/WalletOnboardingSuccess.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable functional/immutable-data */ -import * as React from "react"; -import { SafeAreaView, View } from "react-native"; -import { ButtonSolid, Pictogram } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import { InfoScreenComponent } from "../../../../components/infoScreen/InfoScreenComponent"; -import I18n from "../../../../i18n"; - -type WalletOnboardingSuccessProps = { - onContinue: () => void; -}; - -/** - * Component that shows a success message after the wallet onboarding process is completed - * TODO: Define the desired design of this component - */ -const WalletOnboardingSuccess = ({ - onContinue -}: WalletOnboardingSuccessProps) => ( - - } - title={I18n.t("wallet.onboarding.success.title")} - body={} - /> - -); - -type ContinueButtonProps = { - onPress: () => void; -}; - -const ContinueButton = ({ onPress }: ContinueButtonProps) => ( - - - -); - -export default WalletOnboardingSuccess; diff --git a/ts/features/walletV3/onboarding/hooks/useWalletOnboardingWebView.tsx b/ts/features/walletV3/onboarding/hooks/useWalletOnboardingWebView.tsx deleted file mode 100644 index 17edc36894e..00000000000 --- a/ts/features/walletV3/onboarding/hooks/useWalletOnboardingWebView.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from "react"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils"; -import { - WebViewErrorEvent, - WebViewHttpErrorEvent -} from "react-native-webview/lib/WebViewTypes"; - -import { OnboardingOutcomeFailure, OnboardingOutcomeSuccess } from "../types"; -import { NetworkError } from "../../../../utils/errors"; -import { WalletCreateResponse } from "../../../../../definitions/pagopa/walletv3/WalletCreateResponse"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { walletOnboardingStartupSelector } from "../store"; -import { walletStartOnboarding } from "../store/actions"; -import { - ONBOARDING_CALLBACK_URL_SCHEMA, - extractOnboardingResult -} from "../utils"; - -/** - * Function that extracts the uri to be loaded in the webview from the onboarding startup result pot - */ -const extractOnboardingWebViewUri = ( - onboardingStartupResult: pot.Pot -) => - pot.getOrElse( - pot.map(onboardingStartupResult, result => encodeURI(result.redirectUrl)), - "" - ); - -export type WalletOnboardingWebViewProps = { - onSuccess?: (outcome: OnboardingOutcomeSuccess, walletId: string) => void; - onFailure?: (outcome: OnboardingOutcomeFailure) => void; - onError?: ( - error?: WebViewErrorEvent | WebViewHttpErrorEvent | NetworkError - ) => void; - onDismiss?: () => void; -}; - -/** - * This hook handles the onboarding webview flow and returns a function to start the onboarding - * @param onSuccess callback called when the onboarding is successful - * @param onFailure callback called when the onboarding is failed - * @param onError callback called when an error occurs - */ -export const useWalletOnboardingWebView = ( - props: WalletOnboardingWebViewProps -) => { - const { onSuccess, onError, onFailure, onDismiss } = props; - const onboardingStartupResult = useIOSelector( - walletOnboardingStartupSelector - ); - const dispatch = useIODispatch(); - - const handleResultOnboarding = React.useCallback( - (url: string) => { - pipe( - url, - extractOnboardingResult, - O.fromNullable, - O.map(result => { - if (result.status === "SUCCESS") { - onSuccess?.( - result.outcome as OnboardingOutcomeSuccess, - result.walletId - ); - } else if (result.status === "FAILURE") { - onFailure?.(result.outcome as OnboardingOutcomeFailure); - } - }) - ); - }, - [onSuccess, onFailure] - ); - - const openOnboardingWebView = React.useCallback(async () => { - try { - const resultUrl = await openAuthenticationSession( - extractOnboardingWebViewUri(onboardingStartupResult), - ONBOARDING_CALLBACK_URL_SCHEMA - ); - handleResultOnboarding(resultUrl); - } catch (err) { - onDismiss?.(); - } - }, [onboardingStartupResult, handleResultOnboarding, onDismiss]); - - React.useEffect( - () => () => { - dispatch(walletStartOnboarding.cancel()); - }, - [dispatch] - ); - - React.useEffect(() => { - if (pot.isError(onboardingStartupResult) && onError) { - onError(onboardingStartupResult.error); - } - if ( - !pot.isError(onboardingStartupResult) && - !pot.isLoading(onboardingStartupResult) - ) { - void openOnboardingWebView(); - } - }, [onboardingStartupResult, openOnboardingWebView, onError]); - - const startOnboarding = (paymentMethodId: string) => { - if (!pot.isLoading(onboardingStartupResult)) { - dispatch(walletStartOnboarding.request({ paymentMethodId })); - } - }; - - return { - startOnboarding - }; -}; diff --git a/ts/features/walletV3/onboarding/navigation/navigator.tsx b/ts/features/walletV3/onboarding/navigation/navigator.tsx deleted file mode 100644 index dc31d7fb89e..00000000000 --- a/ts/features/walletV3/onboarding/navigation/navigator.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ParamListBase } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp -} from "@react-navigation/stack"; -import React from "react"; -import { isGestureEnabled } from "../../../../utils/navigation"; -import WalletOnboardingStartScreen, { - WalletOnboardingFeedbackScreenParams -} from "../screens/WalletOnboardingFeedbackScreen"; -import WalletOnboardingSelectPaymentMethodScreen from "../screens/WalletOnboardingSelectPaymentMethodScreen"; - -export const WalletOnboardingRoutes = { - WALLET_ONBOARDING_MAIN: "WALLET_ONBOARDING_MAIN", - WALLET_ONBOARDING_SELECT_PAYMENT_METHOD: - "WALLET_ONBOARDING_SELECT_PAYMENT_METHOD", - WALLET_ONBOARDING_RESULT_FEEDBACK: "WALLET_ONBOARDING_RESULT_FEEDBACK" -} as const; - -export type WalletOnboardingParamsList = { - [WalletOnboardingRoutes.WALLET_ONBOARDING_MAIN]: undefined; - [WalletOnboardingRoutes.WALLET_ONBOARDING_RESULT_FEEDBACK]: WalletOnboardingFeedbackScreenParams; - [WalletOnboardingRoutes.WALLET_ONBOARDING_SELECT_PAYMENT_METHOD]: undefined; -}; - -const Stack = createStackNavigator(); - -export const WalletOnboardingNavigator = () => ( - - - - -); - -export type WalletOnboardingStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; - -export type WalletOnboardingStackNavigation = - WalletOnboardingStackNavigationProp< - WalletOnboardingParamsList, - keyof WalletOnboardingParamsList - >; diff --git a/ts/features/walletV3/onboarding/screens/WalletOnboardingFeedbackScreen.tsx b/ts/features/walletV3/onboarding/screens/WalletOnboardingFeedbackScreen.tsx deleted file mode 100644 index 35969b7d4e0..00000000000 --- a/ts/features/walletV3/onboarding/screens/WalletOnboardingFeedbackScreen.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable functional/immutable-data */ -import * as React from "react"; - -import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; -import { View } from "react-native"; - -import { WalletOnboardingParamsList } from "../navigation/navigator"; - -import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import WalletOnboardingSuccess from "../components/WalletOnboardingSuccess"; -import { OnboardingResult } from "../types"; -import WalletOnboardingError from "../components/WalletOnboardingError"; -import { WalletDetailsRoutes } from "../../details/navigation/navigator"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; - -export type WalletOnboardingFeedbackScreenParams = { - onboardingResult: OnboardingResult; -}; - -type WalletOnboardingFeedbackScreenRouteProps = RouteProp< - WalletOnboardingParamsList, - "WALLET_ONBOARDING_RESULT_FEEDBACK" ->; - -const WalletOnboardingFeedbackScreen = () => { - const navigation = useNavigation>(); - const route = useRoute(); - const { onboardingResult } = route.params; - - const handleContinueButton = () => { - if (onboardingResult && onboardingResult.status === "SUCCESS") { - navigation.replace(WalletDetailsRoutes.WALLET_DETAILS_MAIN, { - screen: WalletDetailsRoutes.WALLET_DETAILS_SCREEN, - params: { - walletId: onboardingResult.walletId - } - }); - } - }; - - // If the onboarding process is completed (with a success or not), we display the result content feedback - return ( - navigation.pop()} - onContinue={handleContinueButton} - /> - ); -}; - -type OnboardingResultContentProps = { - onboardingResult: OnboardingResult; - onClose: () => void; - onContinue: () => void; -}; - -/** - * This component is used to display the result of the onboarding process - * @param onboardingResult the result of the onboarding process - * @param onContinue callback to be called when the user tap on 'Continue' button into success screen - * @param onClose callback to be called when the user tap on 'Close' button into error screen - */ -const OnboardingResultContent = ({ - onboardingResult, - onContinue, - onClose -}: OnboardingResultContentProps) => ( - - {onboardingResult.status === "SUCCESS" && ( - - )} - {(onboardingResult.status === "ERROR" || - onboardingResult.status === "FAILURE") && ( - - )} - -); - -export default WalletOnboardingFeedbackScreen; diff --git a/ts/features/walletV3/onboarding/screens/WalletOnboardingSelectPaymentMethodScreen.tsx b/ts/features/walletV3/onboarding/screens/WalletOnboardingSelectPaymentMethodScreen.tsx deleted file mode 100644 index 6d576443844..00000000000 --- a/ts/features/walletV3/onboarding/screens/WalletOnboardingSelectPaymentMethodScreen.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from "react"; -import { SafeAreaView } from "react-native"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; - -import { Body, H2, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; - -import I18n from "../../../../i18n"; -import { - WalletOnboardingRoutes, - WalletOnboardingStackNavigation -} from "../navigation/navigator"; -import TopScreenComponent from "../../../../components/screens/TopScreenComponent"; -import WalletOnboardingPaymentMethodsList from "../components/WalletOnboardingPaymentMethodsList"; -import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; -import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { walletGetPaymentMethods } from "../store/actions"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { - isLoadingPaymentMethodsSelector, - walletOnboardingPaymentMethodsSelector -} from "../store"; -import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; -import { PaymentMethodStatusEnum } from "../../../../../definitions/pagopa/walletv3/PaymentMethodStatus"; -import { useWalletOnboardingWebView } from "../hooks/useWalletOnboardingWebView"; -import { OnboardingOutcomeEnum, OnboardingResult } from "../types"; - -const WalletOnboardingSelectPaymentMethodScreen = () => { - const navigation = useNavigation(); - const dispatch = useIODispatch(); - const isLoadingPaymentMethods = useIOSelector( - isLoadingPaymentMethodsSelector - ); - const paymentMethodsPot = useIOSelector( - walletOnboardingPaymentMethodsSelector - ); - const availablePaymentMethods = pipe( - pot.getOrElse( - pot.map(paymentMethodsPot, el => el.paymentMethods), - null - ), - O.fromNullable, - O.map(el => el.filter(el => el.status === PaymentMethodStatusEnum.ENABLED)), - O.getOrElseW(() => []) - ); - - const { startOnboarding } = useWalletOnboardingWebView({ - onSuccess: (outcome, walletId) => - navigateToFeedbackPage({ status: "SUCCESS", outcome, walletId }), - onFailure: outcome => - navigateToFeedbackPage({ status: "FAILURE", outcome }), - onError: () => - navigateToFeedbackPage({ - status: "ERROR", - outcome: OnboardingOutcomeEnum.GENERIC_ERROR - }) - }); - - useOnFirstRender(() => { - dispatch(walletGetPaymentMethods.request()); - }); - - const handleSelectedPaymentMethod = ( - selectedPaymentMethod: PaymentMethodResponse - ) => { - startOnboarding(selectedPaymentMethod.id); - }; - - const navigateToFeedbackPage = (onboardingResult: OnboardingResult) => { - navigation.navigate( - WalletOnboardingRoutes.WALLET_ONBOARDING_RESULT_FEEDBACK, - { - onboardingResult - } - ); - }; - - return ( - - {pot.isError(paymentMethodsPot) ? ( - dispatch(walletGetPaymentMethods.request()) - }} - /> - ) : ( - - } - isLoading={isLoadingPaymentMethods} - onSelectPaymentMethod={handleSelectedPaymentMethod} - paymentMethods={availablePaymentMethods} - /> - - )} - - ); -}; - -const PaymentMethodsHeading = () => ( - <> -

{I18n.t("wallet.onboarding.paymentMethodsList.header.title")}

- - - {I18n.t("wallet.onboarding.paymentMethodsList.header.subtitle")} - - - -); - -export default WalletOnboardingSelectPaymentMethodScreen; diff --git a/ts/features/walletV3/onboarding/store/actions/index.ts b/ts/features/walletV3/onboarding/store/actions/index.ts deleted file mode 100644 index 90a55a271bc..00000000000 --- a/ts/features/walletV3/onboarding/store/actions/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ActionType, createAsyncAction } from "typesafe-actions"; -import { NetworkError } from "../../../../../utils/errors"; -import { WalletCreateResponse } from "../../../../../../definitions/pagopa/walletv3/WalletCreateResponse"; -import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; - -export const walletGetPaymentMethods = createAsyncAction( - "WALLET_GET_PAYMENT_METHODS_REQUEST", - "WALLET_GET_PAYMENT_METHODS_SUCCESS", - "WALLET_GET_PAYMENT_METHODS_FAILURE", - "WALLET_GET_PAYMENT_METHODS_CANCEL" -)(); - -export type WalletOnboardingStartPayload = { - paymentMethodId: string; -}; - -export const walletStartOnboarding = createAsyncAction( - "WALLET_ONBOARDING_START_REQUEST", - "WALLET_ONBOARDING_START_SUCCESS", - "WALLET_ONBOARDING_START_FAILURE", - "WALLET_ONBOARDING_START_CANCEL" -)(); - -export type WalletOnboardingActions = - | ActionType - | ActionType; diff --git a/ts/features/walletV3/onboarding/store/index.ts b/ts/features/walletV3/onboarding/store/index.ts deleted file mode 100644 index 88784a408ef..00000000000 --- a/ts/features/walletV3/onboarding/store/index.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as _ from "lodash"; -import { createSelector } from "reselect"; -import { getType } from "typesafe-actions"; -import { Action } from "../../../../store/actions/types"; -import { NetworkError } from "../../../../utils/errors"; -import { GlobalState } from "../../../../store/reducers/types"; -import { WalletCreateResponse } from "../../../../../definitions/pagopa/walletv3/WalletCreateResponse"; -import { PaymentMethodsResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; - -import { walletGetPaymentMethods, walletStartOnboarding } from "./actions"; - -export type WalletOnboardingState = { - result: pot.Pot; - paymentMethods: pot.Pot; -}; - -const INITIAL_STATE: WalletOnboardingState = { - result: pot.none, - paymentMethods: pot.noneLoading -}; - -const walletOnboardingReducer = ( - state: WalletOnboardingState = INITIAL_STATE, - action: Action -): WalletOnboardingState => { - switch (action.type) { - // START ONBOARDING ACTIONS - case getType(walletStartOnboarding.request): - return { - ...state, - result: pot.toLoading(pot.none) - }; - case getType(walletStartOnboarding.success): - return { - ...state, - result: pot.some(action.payload as WalletCreateResponse) - }; - case getType(walletStartOnboarding.failure): - return { - ...state, - result: pot.toError(state.result, action.payload) - }; - case getType(walletStartOnboarding.cancel): - return { - ...state, - result: pot.none - }; - // GET ONBOARDABLE PAYMENT METHODS LIST - case getType(walletGetPaymentMethods.request): - return { - ...state, - paymentMethods: pot.toLoading(pot.none) - }; - case getType(walletGetPaymentMethods.success): - return { - ...state, - paymentMethods: pot.some(action.payload) - }; - case getType(walletGetPaymentMethods.failure): - return { - ...state, - paymentMethods: pot.toError(state.paymentMethods, action.payload) - }; - case getType(walletGetPaymentMethods.cancel): - return { - ...state, - paymentMethods: pot.none - }; - } - return state; -}; - -const walletOnboardingSelector = (state: GlobalState) => - state.features.wallet.onboarding; - -export const walletOnboardingStartupSelector = createSelector( - walletOnboardingSelector, - onboarding => onboarding.result -); - -export const walletOnboardingPaymentMethodsSelector = createSelector( - walletOnboardingSelector, - onboarding => onboarding.paymentMethods -); - -export const isLoadingPaymentMethodsSelector = createSelector( - walletOnboardingPaymentMethodsSelector, - paymentMethods => pot.isLoading(paymentMethods) -); - -export default walletOnboardingReducer; diff --git a/ts/features/walletV3/onboarding/types/index.ts b/ts/features/walletV3/onboarding/types/index.ts deleted file mode 100644 index 21ba156e329..00000000000 --- a/ts/features/walletV3/onboarding/types/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * All the possible outcomes of the onboarding process - */ -export enum OnboardingOutcomeEnum { - SUCCESS = "0", - GENERIC_ERROR = "1", - AUTH_ERROR = "2", - TIMEOUT = "4", - CANCELED_BY_USER = "8", - INVALID_SESSION = "14", - ALREADY_ONBOARDED = "15" -} - -export type OnboardingOutcome = `${OnboardingOutcomeEnum}`; - -export type OnboardingStatus = "SUCCESS" | "FAILURE" | "ERROR"; - -export type OnboardingOutcomeFailure = Exclude< - OnboardingOutcome, - `${OnboardingOutcomeEnum.SUCCESS}` ->; - -export type OnboardingOutcomeSuccess = Extract< - OnboardingOutcome, - `${OnboardingOutcomeEnum.SUCCESS}` ->; - -/** - * Rapresents the result of onboarding process when it is successfully done - */ -export type OnboardingSuccess = { - status: "SUCCESS"; - outcome: OnboardingOutcome; - walletId: string; -}; - -/** - * Rapresents the result of onboarding process when it is failed by the returned webview outcome - */ -export type OnboardingFailure = { - status: "FAILURE"; - outcome: OnboardingOutcomeFailure; -}; - -/** - * Rapresents the result of onboarding process when it has a generic error not based on the webview outcome - */ -export type OnboardingWebViewError = { - status: "ERROR"; - outcome: OnboardingOutcomeFailure; -}; - -export type OnboardingResult = - | OnboardingSuccess - | OnboardingFailure - | OnboardingWebViewError; diff --git a/ts/features/walletV3/onboarding/utils/index.ts b/ts/features/walletV3/onboarding/utils/index.ts deleted file mode 100644 index b3abbb14557..00000000000 --- a/ts/features/walletV3/onboarding/utils/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { IOPictograms } from "@pagopa/io-app-design-system"; - -import URLParse from "url-parse"; -import { - OnboardingWebViewError, - OnboardingOutcome, - OnboardingOutcomeEnum, - OnboardingOutcomeFailure, - OnboardingResult, - OnboardingStatus -} from "../types"; -import { isStringNullyOrEmpty } from "../../../../utils/strings"; - -// List of outcomes that are considered successful -export const successOutcomes: ReadonlyArray = ["0"]; - -export const ONBOARDING_FAQ_ENABLE_3DS = "https://io.italia.it/faq/#n3_3"; - -export const ONBOARDING_CALLBACK_URL_SCHEMA = "iowallet"; - -export const ONBOARDING_OUTCOME_PATH = "/wallets/outcomes"; - -export const ONBOARDING_OUTCOME_ERROR_PICTOGRAM: Record< - OnboardingOutcomeFailure, - IOPictograms -> = { - [OnboardingOutcomeEnum.GENERIC_ERROR]: "umbrellaNew", - [OnboardingOutcomeEnum.AUTH_ERROR]: "accessDenied", - [OnboardingOutcomeEnum.TIMEOUT]: "time", - [OnboardingOutcomeEnum.CANCELED_BY_USER]: "trash", - [OnboardingOutcomeEnum.INVALID_SESSION]: "umbrellaNew", - [OnboardingOutcomeEnum.ALREADY_ONBOARDED]: "success" -}; - -/** - * Function to get the onboarding status from the given outcome - * @param outcome outcome to get the status from - * @returns the status of the onboarding - */ -export const getOutcomeStatus = ( - outcome: OnboardingOutcome -): OnboardingStatus => - successOutcomes.includes(outcome) ? "SUCCESS" : "FAILURE"; - -/** - * Function to extract the onboarding result from the url of the webview - * It will return a {@link OnboardingWebViewError} if the url is not from the onboarding result page - * @param url url to extract the onboarding result from - * @returns an {@link OnboardingResult} - */ -export const extractOnboardingResult = (url: string): OnboardingResult => - pipe( - new URLParse(url, true), - O.fromPredicate(urlParse => - urlParse.pathname.includes(ONBOARDING_OUTCOME_PATH) - ), - O.map(urlParse => ({ - outcome: urlParse.query.outcome as OnboardingOutcome, - walletId: urlParse.query.walletId as string - })), - O.filter(result => !isStringNullyOrEmpty(result.outcome)), - O.map(result => ({ - status: getOutcomeStatus(result.outcome), - outcome: result.outcome as OnboardingOutcomeFailure, - walletId: result.walletId - })), - O.getOrElseW( - () => - ({ - status: "ERROR", - outcome: OnboardingOutcomeEnum.GENERIC_ERROR - } as OnboardingWebViewError) - ) - ); diff --git a/ts/features/walletV3/payment/api/client.ts b/ts/features/walletV3/payment/api/client.ts deleted file mode 100644 index ea06a47c2ad..00000000000 --- a/ts/features/walletV3/payment/api/client.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createClient } from "../../../../../definitions/pagopa/ecommerce/client"; -import { defaultRetryingFetch } from "../../../../utils/fetch"; - -const createPaymentClient = (baseUrl: string, token: string) => - createClient<"walletToken">({ - baseUrl, - basePath: "/ecommerce/io/v1", - fetchApi: defaultRetryingFetch(), - withDefaults: op => params => { - const paramsWithDefaults = { - ...params, - walletToken: token - } as Parameters[0]; - - return op(paramsWithDefaults); - } - }); - -export type PaymentClient = ReturnType; - -export { createPaymentClient }; diff --git a/ts/features/walletV3/payment/navigation/navigator.tsx b/ts/features/walletV3/payment/navigation/navigator.tsx deleted file mode 100644 index 4093d787efa..00000000000 --- a/ts/features/walletV3/payment/navigation/navigator.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { ParamListBase } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp -} from "@react-navigation/stack"; -import React from "react"; -import { isGestureEnabled } from "../../../../utils/navigation"; -import { WalletPaymentDetailScreen } from "../screens/WalletPaymentDetailScreen"; -import { WalletPaymentInputFiscalCodeScreen } from "../screens/WalletPaymentInputFiscalCodeScreen"; -import { WalletPaymentInputNoticeNumberScreen } from "../screens/WalletPaymentInputNoticeNumberScreen"; -import { WalletPaymentPickMethodScreen } from "../screens/WalletPaymentPickMethodScreen"; -import { WalletPaymentOutcomeScreen } from "../screens/WalletPaymentOutcomeScreen"; -import { WalletPaymentPickPspScreen } from "../screens/WalletPaymentPickPspScreen"; -import { WalletPaymentConfirmScreen } from "../screens/WalletPaymentConfirmScreen"; -import { WalletPaymentParamsList } from "./params"; -import { WalletPaymentRoutes } from "./routes"; - -const Stack = createStackNavigator(); - -export const WalletPaymentNavigator = () => ( - - - - - - - - - -); - -export type WalletPaymentStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; - -export type WalletPaymentStackNavigation = WalletPaymentStackNavigationProp< - WalletPaymentParamsList, - keyof WalletPaymentParamsList ->; diff --git a/ts/features/walletV3/payment/navigation/params.ts b/ts/features/walletV3/payment/navigation/params.ts deleted file mode 100644 index 2882dc9b783..00000000000 --- a/ts/features/walletV3/payment/navigation/params.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { WalletPaymentDetailScreenNavigationParams } from "../screens/WalletPaymentDetailScreen"; -import { WalletPaymentInputFiscalCodeScreenNavigationParams } from "../screens/WalletPaymentInputFiscalCodeScreen"; -import { WalletPaymentOutcomeScreenNavigationParams } from "../screens/WalletPaymentOutcomeScreen"; -import { WalletPaymentRoutes } from "./routes"; - -export type WalletPaymentParamsList = { - [WalletPaymentRoutes.WALLET_PAYMENT_MAIN]: undefined; - [WalletPaymentRoutes.WALLET_PAYMENT_INPUT_NOTICE_NUMBER]: undefined; - [WalletPaymentRoutes.WALLET_PAYMENT_INPUT_FISCAL_CODE]: WalletPaymentInputFiscalCodeScreenNavigationParams; - [WalletPaymentRoutes.WALLET_PAYMENT_DETAIL]: WalletPaymentDetailScreenNavigationParams; - [WalletPaymentRoutes.WALLET_PAYMENT_PICK_METHOD]: undefined; - [WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP]: undefined; - [WalletPaymentRoutes.WALLET_PAYMENT_CONFIRM]: undefined; - [WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME]: WalletPaymentOutcomeScreenNavigationParams; -}; diff --git a/ts/features/walletV3/payment/navigation/routes.ts b/ts/features/walletV3/payment/navigation/routes.ts deleted file mode 100644 index 372daa211ef..00000000000 --- a/ts/features/walletV3/payment/navigation/routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const WalletPaymentRoutes = { - WALLET_PAYMENT_MAIN: "WALLET_PAYMENT_MAIN", - WALLET_PAYMENT_INPUT_NOTICE_NUMBER: "WALLET_PAYMENT_INPUT_NOTICE_NUMBER", - WALLET_PAYMENT_INPUT_FISCAL_CODE: "WALLET_PAYMENT_INPUT_FISCAL_CODE", - WALLET_PAYMENT_DETAIL: "WALLET_PAYMENT_DETAIL", - WALLET_PAYMENT_PICK_METHOD: "WALLET_PAYMENT_PICK_METHOD", - WALLET_PAYMENT_PICK_PSP: "WALLET_PAYMENT_PICK_PSP", - WALLET_PAYMENT_CONFIRM: "WALLET_PAYMENT_CONFIRM", - WALLET_PAYMENT_OUTCOME: "WALLET_PAYMENT_OUTCOME" -} as const; diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetUserWallets.ts b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetUserWallets.ts deleted file mode 100644 index e087e8ac340..00000000000 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetUserWallets.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 { SagaCallReturnType } from "../../../../../types/utils"; -import { getGenericError, getNetworkError } from "../../../../../utils/errors"; -import { readablePrivacyReport } from "../../../../../utils/reporters"; -import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; -import { WalletClient } from "../../../common/api/client"; -import { walletPaymentGetUserWallets } from "../../store/actions/networking"; - -export function* handleWalletPaymentGetUserWallets( - getWalletsByIdUser: WalletClient["getWalletsByIdUser"], - action: ActionType<(typeof walletPaymentGetUserWallets)["request"]> -) { - const getWalletsByIdUserRequest = getWalletsByIdUser({}); - - try { - const getWalletsByIdUserResult = (yield* call( - withRefreshApiCall, - getWalletsByIdUserRequest, - action - )) as SagaCallReturnType; - - yield* put( - pipe( - getWalletsByIdUserResult, - E.fold( - error => - walletPaymentGetUserWallets.failure( - getGenericError(new Error(readablePrivacyReport(error))) - ), - res => { - if (res.status === 200) { - return walletPaymentGetUserWallets.success(res.value); - } - return walletPaymentGetUserWallets.failure({ - ...getGenericError(new Error(`Error: ${res.status}`)) - }); - } - ) - ) - ); - } catch (e) { - yield* put(walletPaymentGetUserWallets.failure({ ...getNetworkError(e) })); - } -} diff --git a/ts/features/walletV3/payment/store/__tests__/store.test.ts b/ts/features/walletV3/payment/store/__tests__/store.test.ts deleted file mode 100644 index 1ea468f021c..00000000000 --- a/ts/features/walletV3/payment/store/__tests__/store.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { applicationChangeState } from "../../../../../store/actions/application"; -import { appReducer } from "../../../../../store/reducers"; -import { WalletPaymentState } from "../reducers"; - -const INITIAL_STATE: WalletPaymentState = { - sessionToken: pot.none, - paymentDetails: pot.none, - userWallets: pot.none, - allPaymentMethods: pot.none, - pspList: pot.none, - chosenPaymentMethod: O.none, - chosenPsp: O.none, - transaction: pot.none, - authorizationUrl: pot.none -}; - -describe("Test Wallet reducer", () => { - it("should have initial state at startup", () => { - const globalState = appReducer(undefined, applicationChangeState("active")); - expect(globalState.features.wallet.payment).toStrictEqual(INITIAL_STATE); - }); -}); diff --git a/ts/features/walletV3/payment/store/actions/index.ts b/ts/features/walletV3/payment/store/actions/index.ts deleted file mode 100644 index 994c2d5cfd3..00000000000 --- a/ts/features/walletV3/payment/store/actions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WalletPaymentNetworkingActions } from "./networking"; -import { WalletPaymentOrchestrationActions } from "./orchestration"; - -export type WalletPaymentActions = - | WalletPaymentNetworkingActions - | WalletPaymentOrchestrationActions; diff --git a/ts/features/walletV3/payment/store/actions/networking.ts b/ts/features/walletV3/payment/store/actions/networking.ts deleted file mode 100644 index 9065a94feac..00000000000 --- a/ts/features/walletV3/payment/store/actions/networking.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ActionType, createAsyncAction } from "typesafe-actions"; -import { AmountEuroCents } from "../../../../../../definitions/pagopa/ecommerce/AmountEuroCents"; -import { CalculateFeeResponse } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeResponse"; -import { NewTransactionRequest } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionRequest"; -import { NewTransactionResponse } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; -import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; -import { RequestAuthorizationResponse } from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationResponse"; -import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; -import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; -import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; -import { NetworkError } from "../../../../../utils/errors"; -import { WalletPaymentFailure } from "../../types/failure"; -import { NewSessionTokenResponse } from "../../../../../../definitions/pagopa/ecommerce/NewSessionTokenResponse"; -import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; -import { CalculateFeeRequest } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeRequest"; - -export const walletPaymentNewSessionToken = createAsyncAction( - "WALLET_PAYMENT_NEW_SESSION_TOKEN_REQUEST", - "WALLET_PAYMENT_NEW_SESSION_TOKEN_SUCCESS", - "WALLET_PAYMENT_NEW_SESSION_TOKEN_FAILURE" -)(); - -export const walletPaymentGetDetails = createAsyncAction( - "WALLET_PAYMENT_GET_DETAILS_REQUEST", - "WALLET_PAYMENT_GET_DETAILS_SUCCESS", - "WALLET_PAYMENT_GET_DETAILS_FAILURE" -)(); - -export const walletPaymentGetAllMethods = createAsyncAction( - "WALLET_PAYMENT_GET_ALL_METHODS_REQUEST", - "WALLET_PAYMENT_GET_ALL_METHODS_SUCCESS", - "WALLET_PAYMENT_GET_ALL_METHODS_FAILURE" -)(); - -export const walletPaymentGetUserWallets = createAsyncAction( - "WALLET_PAYMENT_GET_USER_WALLETS_REQUEST", - "WALLET_PAYMENT_GET_USER_WALLETS_SUCCESS", - "WALLET_PAYMENT_GET_USER_WALLETS_FAILURE" -)(); - -export const walletPaymentCalculateFees = createAsyncAction( - "WALLET_PAYMET_CALCULATE_FEES_REQUEST", - "WALLET_PAYMET_CALCULATE_FEES_SUCCESS", - "WALLET_PAYMET_CALCULATE_FEES_FAILURE" -)< - CalculateFeeRequest & { paymentMethodId: string }, - CalculateFeeResponse, - NetworkError ->(); - -export const walletPaymentCreateTransaction = createAsyncAction( - "WALLET_PAYMENT_CREATE_TRANSACTION_REQUEST", - "WALLET_PAYMENT_CREATE_TRANSACTION_SUCCESS", - "WALLET_PAYMENT_CREATE_TRANSACTION_FAILURE" -)< - NewTransactionRequest, - NewTransactionResponse, - NetworkError | WalletPaymentFailure ->(); - -export const walletPaymentGetTransactionInfo = createAsyncAction( - "WALLET_PAYMENT_GET_TRANSACTION_INFO_REQUEST", - "WALLET_PAYMENT_GET_TRANSACTION_INFO_SUCCESS", - "WALLET_PAYMENT_GET_TRANSACTION_INFO_FAILURE" -)<{ transactionId: string }, TransactionInfo, NetworkError>(); - -export const walletPaymentDeleteTransaction = createAsyncAction( - "WALLET_PAYMENT_DELETE_TRANSACTION_REQUEST", - "WALLET_PAYMENT_DELETE_TRANSACTION_SUCCESS", - "WALLET_PAYMENT_DELETE_TRANSACTION_FAILURE" -)(); - -export type WalletPaymentAuthorizePayload = { - transactionId: string; - walletId: string; - pspId: string; - paymentAmount: AmountEuroCents; - paymentFees: AmountEuroCents; -}; - -export const walletPaymentAuthorization = createAsyncAction( - "WALLET_PAYMENT_AUTH_REQUEST", - "WALLET_PAYMENT_AUTH_SUCCESS", - "WALLET_PAYMENT_AUTH_FAILURE", - "WALLET_PAYMENT_AUTH_CANCEL" -)< - WalletPaymentAuthorizePayload, - RequestAuthorizationResponse, - NetworkError, - undefined ->(); - -export type WalletPaymentNetworkingActions = - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType; diff --git a/ts/features/walletV3/payment/store/actions/orchestration.ts b/ts/features/walletV3/payment/store/actions/orchestration.ts deleted file mode 100644 index cba31c50fa9..00000000000 --- a/ts/features/walletV3/payment/store/actions/orchestration.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ActionType, createStandardAction } from "typesafe-actions"; -import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; -import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; - -/** - * Action to initialize the state of a payment, optionally you can specify the route to go back to - * after the payment is completed or cancelled (default is the popToTop route) - */ -export const walletPaymentInitState = createStandardAction( - "WALLET_PAYMENT_INIT_STATE" -)(); - -export const walletPaymentPickPaymentMethod = createStandardAction( - "WALLET_PAYMENT_PICK_PAYMENT_METHOD" -)(); - -export const walletPaymentPickPsp = createStandardAction( - "WALLET_PAYMENT_PICK_PSP" -)(); - -export const walletPaymentResetPickedPsp = createStandardAction( - "WALLET_PAYMENT_RESET_PICKED_PSP" -)(); - -export type WalletPaymentOrchestrationActions = - | ActionType - | ActionType - | ActionType - | ActionType; diff --git a/ts/features/walletV3/transaction/navigation/navigator.tsx b/ts/features/walletV3/transaction/navigation/navigator.tsx deleted file mode 100644 index 9b26df0638d..00000000000 --- a/ts/features/walletV3/transaction/navigation/navigator.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ParamListBase } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp -} from "@react-navigation/stack"; -import React from "react"; -import { isGestureEnabled } from "../../../../utils/navigation"; -import WalletTransactionDetailsScreen, { - WalletTransactionDetailsScreenParams -} from "../screens/WalletTransactionDetailsScreen"; -import WalletTransactionOperationDetailsScreen, { - WalletTransactionOperationDetailsScreenParams -} from "../screens/WalletTransactionOperationDetails"; - -export const WalletTransactionRoutes = { - WALLET_TRANSACTION_MAIN: "WALLET_TRANSACTION_MAIN", - WALLET_TRANSACTION_DETAILS: "WALLET_TRANSACTION_DETAILS", - WALLET_TRANSACTION_OPERATION_DETAILS: "WALLET_TRANSACTION_OPERATION_DETAILS" -} as const; - -export type WalletTransactionParamsList = { - [WalletTransactionRoutes.WALLET_TRANSACTION_MAIN]: undefined; - [WalletTransactionRoutes.WALLET_TRANSACTION_DETAILS]: WalletTransactionDetailsScreenParams; - [WalletTransactionRoutes.WALLET_TRANSACTION_OPERATION_DETAILS]: WalletTransactionOperationDetailsScreenParams; -}; - -const Stack = createStackNavigator(); - -export const WalletTransactionNavigator = () => ( - - - - -); - -export type WalletTransactionStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; - -export type WalletTransactionStackNavigation = - WalletTransactionStackNavigationProp< - WalletTransactionParamsList, - keyof WalletTransactionParamsList - >; diff --git a/ts/features/walletV3/transaction/store/actions/index.ts b/ts/features/walletV3/transaction/store/actions/index.ts deleted file mode 100644 index 50f40edff17..00000000000 --- a/ts/features/walletV3/transaction/store/actions/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ActionType, createAsyncAction } from "typesafe-actions"; -import { NetworkError } from "../../../../../utils/errors"; -import { Transaction } from "../../../../../types/pagopa"; - -export type WalletTransactionDetailsPayload = { - transactionId: number; -}; - -export const walletTransactionDetailsGet = createAsyncAction( - "WALLET_TRANSACTION_DETAILS_REQUEST", - "WALLET_TRANSACTION_DETAILS_SUCCESS", - "WALLET_TRANSACTION_DETAILS_FAILURE", - "WALLET_TRANSACTION_DETAILS_CANCEL" -)(); - -export type WalletTransactionActions = ActionType< - typeof walletTransactionDetailsGet ->; diff --git a/ts/features/walletV3/transaction/store/index.ts b/ts/features/walletV3/transaction/store/index.ts deleted file mode 100644 index 2efc50116f9..00000000000 --- a/ts/features/walletV3/transaction/store/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as _ from "lodash"; -import { createSelector } from "reselect"; -import { getType } from "typesafe-actions"; -import { Action } from "../../../../store/actions/types"; -import { NetworkError } from "../../../../utils/errors"; -import { GlobalState } from "../../../../store/reducers/types"; - -import { Transaction } from "../../../../types/pagopa"; -import { walletTransactionDetailsGet } from "./actions"; - -export type WalletTransactionState = { - details: pot.Pot; -}; - -const INITIAL_STATE: WalletTransactionState = { - details: pot.noneLoading -}; - -const walletTransactionReducer = ( - state: WalletTransactionState = INITIAL_STATE, - action: Action -): WalletTransactionState => { - switch (action.type) { - // GET TRANSACTION DETAILS - case getType(walletTransactionDetailsGet.request): - return { - ...state, - details: pot.toLoading(pot.none) - }; - case getType(walletTransactionDetailsGet.success): - return { - ...state, - details: pot.some(action.payload) - }; - case getType(walletTransactionDetailsGet.failure): - return { - ...state, - details: pot.toError(state.details, action.payload) - }; - case getType(walletTransactionDetailsGet.cancel): - return { - ...state, - details: pot.none - }; - } - return state; -}; - -const walletTransactionSelector = (state: GlobalState) => - state.features.wallet.transaction; - -export const walletTransactionDetailsPotSelector = createSelector( - walletTransactionSelector, - transaction => transaction.details -); - -export default walletTransactionReducer; diff --git a/ts/features/zendesk/analytics/index.ts b/ts/features/zendesk/analytics/index.ts index 50459f5db9e..9acc4628f76 100644 --- a/ts/features/zendesk/analytics/index.ts +++ b/ts/features/zendesk/analytics/index.ts @@ -1,4 +1,5 @@ import { getType } from "typesafe-actions"; +import { constVoid } from "fp-ts/lib/function"; import { mixpanel } from "../../../mixpanel"; import { Action } from "../../../store/actions/types"; @@ -15,7 +16,7 @@ import { const trackZendesk = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(zendeskSupportCompleted): case getType(zendeskSupportCancel): @@ -41,10 +42,9 @@ const trackZendesk = reason: getNetworkErrorMessage(action.payload) }); } - return Promise.resolve(); }; const emptyTracking = (_: NonNullable) => (__: Action) => - Promise.resolve(); + constVoid(); export default zendeskEnabled ? trackZendesk : emptyTracking; diff --git a/ts/features/zendesk/navigation/navigator.tsx b/ts/features/zendesk/navigation/navigator.tsx index 6f2956abe2c..dd6424fe3bd 100644 --- a/ts/features/zendesk/navigation/navigator.tsx +++ b/ts/features/zendesk/navigation/navigator.tsx @@ -16,8 +16,7 @@ const Stack = createStackNavigator(); export const ZendeskStackNavigator = () => ( { const dispatch = useIODispatch(); const workUnitCancel = () => dispatch(zendeskSupportCancel()); const workUnitComplete = () => dispatch(zendeskSupportCompleted()); + const profile = useIOSelector(profileSelector); const signatureRequestId = useIOSelector(fciSignatureRequestIdSelector); + const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); + const showRequestSupportContacts = isEmailValidated || !pot.isSome(profile); const route = useRoute>(); @@ -238,14 +245,19 @@ const ZendeskSupportHelpCenter = () => { contentLoaded={markdownContentLoaded} startingRoute={startingRoute} /> - - + + {showRequestSupportContacts && ( + <> + + + + )}
diff --git a/ts/hooks/useHeaderSecondLevel.tsx b/ts/hooks/useHeaderSecondLevel.tsx index 9b752c19776..fa42cd1f753 100644 --- a/ts/hooks/useHeaderSecondLevel.tsx +++ b/ts/hooks/useHeaderSecondLevel.tsx @@ -18,6 +18,7 @@ type CommonProps = { title: string; backAccessibilityLabel?: string; goBack?: () => void; + canGoBack?: boolean; transparent?: boolean; scrollValues?: ScrollValues; }; @@ -65,6 +66,7 @@ export const useHeaderSecondLevel = ({ contextualHelpMarkdown, faqCategories, goBack, + canGoBack = true, supportRequest, secondAction, thirdAction, @@ -79,12 +81,14 @@ export const useHeaderSecondLevel = ({ const navigation = useNavigation(); const headerComponentProps: HeaderProps = React.useMemo(() => { - const baseProps = { - title, - backAccessibilityLabel: - backAccessibilityLabel ?? I18n.t("global.buttons.back"), - goBack: goBack ?? navigation.goBack - }; + const baseProps = canGoBack + ? { + title, + backAccessibilityLabel: + backAccessibilityLabel ?? I18n.t("global.buttons.back"), + goBack: goBack ?? navigation.goBack + } + : { title }; if (supportRequest) { const helpAction = { @@ -127,6 +131,7 @@ export const useHeaderSecondLevel = ({ type: "base" }; }, [ + canGoBack, title, backAccessibilityLabel, goBack, diff --git a/ts/hooks/useStartSupportRequest.ts b/ts/hooks/useStartSupportRequest.ts index 04605fc5c79..b9e7a41a0a0 100644 --- a/ts/hooks/useStartSupportRequest.ts +++ b/ts/hooks/useStartSupportRequest.ts @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { useRoute } from "@react-navigation/native"; import { ToolEnum } from "../../definitions/content/AssistanceToolConfig"; import { ContextualHelpProps, @@ -7,7 +8,6 @@ import { import { zendeskSupportStart } from "../features/zendesk/store/actions"; import { useIODispatch, useIOSelector } from "../store/hooks"; import { assistanceToolConfigSelector } from "../store/reducers/backendStatus"; -import { currentRouteSelector } from "../store/reducers/navigation"; import { FAQsCategoriesType } from "../utils/faq"; import { assistanceToolRemoteConfig, @@ -25,11 +25,7 @@ export const useStartSupportRequest = ({ contextualHelp, contextualHelpMarkdown }: SupportRequestParams) => { - /** - * We have to use the deprecated currentRouteSelector because, at the moment, some components are rendered outside the navigation context. - * TODO: Full usage of navigation header and modal, in order to have always the right context - */ - const currentScreenName = useIOSelector(currentRouteSelector); + const { name: currentScreenName } = useRoute(); const dispatch = useIODispatch(); const assistanceToolConfig = useIOSelector(assistanceToolConfigSelector); diff --git a/ts/hooks/useValidateEmailModal.tsx b/ts/hooks/useValidateEmailModal.tsx deleted file mode 100644 index b40deebdf37..00000000000 --- a/ts/hooks/useValidateEmailModal.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useFocusEffect } from "@react-navigation/native"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { useContext } from "react"; -import RemindEmailValidationOverlay from "../components/RemindEmailValidationOverlay"; -import { LightModalContext } from "../components/ui/LightModal"; -import { useIOSelector } from "../store/hooks"; -import { emailValidationSelector } from "../store/reducers/emailValidation"; -import { isProfileEmailValidatedSelector } from "../store/reducers/profile"; - -export const useValidatedEmailModal = (isOnboarding?: boolean) => { - const { showModal, hideModal } = useContext(LightModalContext); - const isEmailValidatedSelector = useIOSelector( - isProfileEmailValidatedSelector - ); - const { acknowledgeOnEmailValidated } = useIOSelector( - emailValidationSelector - ); - - const isEmailValidated = React.useMemo( - () => - isEmailValidatedSelector && - pipe( - acknowledgeOnEmailValidated, - O.getOrElse(() => true) - ), - [isEmailValidatedSelector, acknowledgeOnEmailValidated] - ); - - useFocusEffect( - React.useCallback(() => { - if (!isEmailValidated) { - showModal(); - } - return () => hideModal(); - }, [hideModal, isEmailValidated, isOnboarding, showModal]) - ); -}; diff --git a/ts/mixpanel.ts b/ts/mixpanel.ts index 07972c6bdb9..2c180c5939e 100644 --- a/ts/mixpanel.ts +++ b/ts/mixpanel.ts @@ -1,13 +1,12 @@ -import { MixpanelInstance } from "react-native-mixpanel"; +import { Mixpanel } from "mixpanel-react-native"; import { mixpanelToken } from "./config"; -import { isAndroid, isIos } from "./utils/platform"; import { getDeviceId } from "./utils/device"; import { GlobalState } from "./store/reducers/types"; import { updateMixpanelSuperProperties } from "./mixpanelConfig/superProperties"; import { updateMixpanelProfileProperties } from "./mixpanelConfig/profileProperties"; // eslint-disable-next-line -export let mixpanel: MixpanelInstance | undefined; +export let mixpanel: Mixpanel | undefined; /** * Initialize mixpanel at start @@ -16,22 +15,22 @@ export const initializeMixPanel = async (state: GlobalState) => { if (mixpanel !== undefined) { return; } - const privateInstance = new MixpanelInstance(mixpanelToken); - await privateInstance.initialize(); + const trackAutomaticEvents = true; + const privateInstance = new Mixpanel(mixpanelToken, trackAutomaticEvents); + await privateInstance.init( + undefined, + undefined, + "https://api-eu.mixpanel.com" + ); mixpanel = privateInstance; // On app first open // On profile page, when user opt-in await setupMixpanel(mixpanel, state); }; -const setupMixpanel = async (mp: MixpanelInstance, state: GlobalState) => { - await mp.optInTracking(); - // on iOS it can be deactivate by invoking a SDK method - // on Android it can be done adding an extra config in AndroidManifest - // see https://help.mixpanel.com/hc/en-us/articles/115004494803-Disable-Geolocation-Collection - if (isIos) { - await mp.disableIpAddressGeolocalization(); - } +const setupMixpanel = async (mp: Mixpanel, state: GlobalState) => { + mp.optInTracking(); + mp.setUseIpAddressForGeolocation(false); await updateMixpanelSuperProperties(state); await updateMixpanelProfileProperties(state); @@ -42,30 +41,17 @@ export const identifyMixpanel = async () => { await mixpanel?.identify(getDeviceId()); }; -export const resetMixpanel = async () => { +export const resetMixpanel = () => { // Reset mixpanel auto generated uniqueId - await mixpanel?.reset(); + mixpanel?.reset(); }; -export const terminateMixpanel = async () => { +export const terminateMixpanel = () => { if (mixpanel) { - await mixpanel.flush(); - await mixpanel.optOutTracking(); + mixpanel.flush(); + mixpanel.optOutTracking(); mixpanel = undefined; } - return Promise.resolve(); -}; - -export const setMixpanelPushNotificationToken = (token: string) => { - if (mixpanel) { - if (isIos) { - return mixpanel.addPushDeviceToken(token); - } - if (isAndroid) { - return mixpanel.setPushRegistrationId(token); - } - } - return Promise.resolve(); }; /** diff --git a/ts/mixpanelConfig/profileProperties.ts b/ts/mixpanelConfig/profileProperties.ts index 0d0a3f7d33a..1b04aec31f0 100644 --- a/ts/mixpanelConfig/profileProperties.ts +++ b/ts/mixpanelConfig/profileProperties.ts @@ -56,7 +56,7 @@ export const updateMixpanelProfileProperties = async ( ); } - await mixpanel.set(profilePropertiesObject); + mixpanel.getPeople().set(profilePropertiesObject); }; const forceUpdate = ( diff --git a/ts/mixpanelConfig/superProperties.ts b/ts/mixpanelConfig/superProperties.ts index 6c29dd23d74..c263b2285f2 100644 --- a/ts/mixpanelConfig/superProperties.ts +++ b/ts/mixpanelConfig/superProperties.ts @@ -67,7 +67,7 @@ export const updateMixpanelSuperProperties = async ( forceUpdate(superPropertiesObject, forceUpdateFor); } - await mixpanel.registerSuperProperties(superPropertiesObject); + mixpanel.registerSuperProperties(superPropertiesObject); }; const forceUpdate = ( diff --git a/ts/navigation/AppStackNavigator.tsx b/ts/navigation/AppStackNavigator.tsx index 5641f27fb5f..b711994cc59 100644 --- a/ts/navigation/AppStackNavigator.tsx +++ b/ts/navigation/AppStackNavigator.tsx @@ -1,11 +1,11 @@ /* eslint-disable functional/immutable-data */ +import { useIOThemeContext } from "@pagopa/io-app-design-system"; import { LinkingOptions, NavigationContainer, NavigationContainerProps } from "@react-navigation/native"; -import * as React from "react"; -import { useRef } from "react"; +import React, { useRef } from "react"; import { View } from "react-native"; import { useStoredExperimentalDesign } from "../common/context/DSExperimentalContext"; import LoadingSpinnerOverlay from "../components/LoadingSpinnerOverlay"; @@ -16,7 +16,7 @@ import { fimsLinkingOptions } from "../features/fims/navigation/navigator"; import { idPayLinkingOptions } from "../features/idpay/common/navigation/linking"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import UADONATION_ROUTES from "../features/uaDonations/navigation/routes"; -import IngressScreen from "../screens/ingress/IngressScreen"; +import { IngressScreen } from "../screens/ingress/IngressScreen"; import { startApplicationInitialization } from "../store/actions/application"; import { setDebugCurrentRouteName } from "../store/actions/debug"; import { useIODispatch, useIOSelector } from "../store/hooks"; @@ -25,19 +25,25 @@ import { isCGNEnabledSelector, isFIMSEnabledSelector } from "../store/reducers/backendStatus"; +import { isNewWalletSectionEnabledSelector } from "../store/reducers/persistedPreferences"; import { StartupStatusEnum, isStartupLoaded } from "../store/reducers/startup"; -import { IONavigationLightTheme } from "../theme/navigations"; +import { + IONavigationDarkTheme, + IONavigationLightTheme +} from "../theme/navigations"; import { isTestEnv } from "../utils/environment"; import { IO_INTERNAL_LINK_PREFIX, IO_UNIVERSAL_LINK_PREFIX } from "../utils/navigation"; +import { SERVICES_ROUTES } from "../features/services/navigation/routes"; import AuthenticatedStackNavigator from "./AuthenticatedStackNavigator"; import NavigationService, { navigationRef, setMainNavigatorReady } from "./NavigationService"; import NotAuthenticatedStackNavigator from "./NotAuthenticatedStackNavigator"; +import { AppParamsList } from "./params/AppParamsList"; import ROUTES from "./routes"; type OnStateChangeStateType = Parameters< @@ -80,8 +86,14 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { const cgnEnabled = useIOSelector(isCGNEnabledSelector); const isFimsEnabled = useIOSelector(isFIMSEnabledSelector) && fimsEnabled; + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); - const linking: LinkingOptions = { + // Dark/Light Mode + const { themeType } = useIOThemeContext(); + + const linking: LinkingOptions = { enabled: !isTestEnv, // disable linking in test env prefixes: [IO_INTERNAL_LINK_PREFIX, IO_UNIVERSAL_LINK_PREFIX], config: { @@ -92,7 +104,11 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { screens: { [MESSAGES_ROUTES.MESSAGES_HOME]: "messages", [ROUTES.WALLET_HOME]: "wallet", - [ROUTES.SERVICES_HOME]: "services", + [SERVICES_ROUTES.SERVICES_HOME]: "services", + // [ROUTES.BARCODE_SCAN]: "scan", + ...(isNewWalletSectionEnabled + ? { [ROUTES.PAYMENTS_HOME]: "payments" } + : {}), [ROUTES.PROFILE_MAIN]: "profile" } }, @@ -112,16 +128,18 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { "card-onboarding-attempts" } }, - [ROUTES.SERVICES_NAVIGATOR]: { + [SERVICES_ROUTES.SERVICES_NAVIGATOR]: { path: "services", screens: { - [ROUTES.SERVICE_DETAIL]: { + [SERVICES_ROUTES.SERVICE_DETAIL]: { path: "service-detail", parse: { activate: activate => activate === "true" } }, - ...(myPortalEnabled && { [ROUTES.SERVICE_WEBVIEW]: "webview" }) + ...(myPortalEnabled && { + [SERVICES_ROUTES.SERVICE_WEBVIEW]: "webview" + }) } }, ...fciLinkingOptions, @@ -136,7 +154,9 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { return ( } diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index b057251f2f1..50d1a59d32f 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -2,7 +2,7 @@ import { createStackNavigator, TransitionPresets } from "@react-navigation/stack"; -import * as React from "react"; +import React from "react"; import WorkunitGenericFailure from "../components/error/WorkunitGenericFailure"; import { fimsEnabled } from "../config"; import { BarcodeScanScreen } from "../features/barcode/screens/BarcodeScanScreen"; @@ -44,24 +44,24 @@ import { IDPayUnsubscriptionRoutes } from "../features/idpay/unsubscription/navigation/navigator"; import UnsupportedDeviceScreen from "../features/lollipop/screens/UnsupportedDeviceScreen"; +import { MessagesStackNavigator } from "../features/messages/navigation/MessagesNavigator"; +import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; +import { WalletNavigator as NewWalletNavigator } from "../features/newWallet/navigation"; +import { WalletRoutes as NewWalletRoutes } from "../features/newWallet/navigation/routes"; +import { WalletBarcodeNavigator } from "../features/payments/barcode/navigation/navigator"; +import { PaymentsBarcodeRoutes } from "../features/payments/barcode/navigation/routes"; +import { PaymentsCheckoutNavigator } from "../features/payments/checkout/navigation/navigator"; +import { PaymentsCheckoutRoutes } from "../features/payments/checkout/navigation/routes"; +import { PaymentsMethodDetailsNavigator } from "../features/payments/details/navigation/navigator"; +import { PaymentsMethodDetailsRoutes } from "../features/payments/details/navigation/routes"; +import { PaymentsOnboardingNavigator } from "../features/payments/onboarding/navigation/navigator"; +import { PaymentsOnboardingRoutes } from "../features/payments/onboarding/navigation/routes"; +import { PaymentsTransactionNavigator } from "../features/payments/transaction/navigation/navigator"; +import { PaymentsTransactionRoutes } from "../features/payments/transaction/navigation/routes"; +import ServicesNavigator from "../features/services/navigation/navigator"; +import { SERVICES_ROUTES } from "../features/services/navigation/routes"; import UADONATION_ROUTES from "../features/uaDonations/navigation/routes"; import { UAWebViewScreen } from "../features/uaDonations/screens/UAWebViewScreen"; -import { WalletBarcodeNavigator } from "../features/walletV3/barcode/navigation/navigator"; -import { WalletBarcodeRoutes } from "../features/walletV3/barcode/navigation/routes"; -import { - WalletDetailsNavigator, - WalletDetailsRoutes -} from "../features/walletV3/details/navigation/navigator"; -import { - WalletOnboardingNavigator, - WalletOnboardingRoutes -} from "../features/walletV3/onboarding/navigation/navigator"; -import { WalletPaymentNavigator } from "../features/walletV3/payment/navigation/navigator"; -import { WalletPaymentRoutes } from "../features/walletV3/payment/navigation/routes"; -import { - WalletTransactionNavigator, - WalletTransactionRoutes -} from "../features/walletV3/transaction/navigation/navigator"; import { ZendeskStackNavigator } from "../features/zendesk/navigation/navigator"; import ZENDESK_ROUTES from "../features/zendesk/navigation/routes"; import { GalleryPermissionInstructionsScreen } from "../screens/misc/GalleryPermissionInstructionsScreen"; @@ -73,15 +73,13 @@ import { isFIMSEnabledSelector, isIdPayEnabledSelector } from "../store/reducers/backendStatus"; +import { isNewWalletSectionEnabledSelector } from "../store/reducers/persistedPreferences"; import { isGestureEnabled } from "../utils/navigation"; -import { MessagesStackNavigator } from "../features/messages/navigation/MessagesNavigator"; -import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import CheckEmailNavigator from "./CheckEmailNavigator"; import OnboardingNavigator from "./OnboardingNavigator"; import { AppParamsList } from "./params/AppParamsList"; import ProfileStackNavigator from "./ProfileNavigator"; import ROUTES from "./routes"; -import ServicesNavigator from "./ServicesNavigator"; import { MainTabNavigator } from "./TabNavigator"; import WalletNavigator from "./WalletNavigator"; @@ -97,14 +95,23 @@ const AuthenticatedStackNavigator = () => { const cgnEnabled = useIOSelector(isCGNEnabledSelector); const isFciEnabled = useIOSelector(isFciEnabledSelector); const isIdPayEnabled = useIOSelector(isIdPayEnabledSelector); + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); return ( - + { options={hideHeaderOptions} component={MessagesStackNavigator} /> + {isNewWalletSectionEnabled ? ( + + ) : ( + + )} - @@ -287,36 +302,36 @@ const AuthenticatedStackNavigator = () => { )} (); const AuthenticationStackNavigator = () => ( + + + + ( component={TestAuthenticationScreen} /> - - ( name={ROUTES.CIE_PIN_TEMP_LOCKED_SCREEN} component={CiePinLockedTemporarilyScreen} /> + + null, + headerTitle: () => null, + headerRight: CloseButton, + headerStyle: { + height: IOVisualCostants.headerHeight, + // shadowOpacity and elevation are set to 0 to hide the shadow under the header + elevation: 0, + shadowOpacity: 0 + }, + headerShown: true + }} + > + + ); diff --git a/ts/navigation/CheckEmailNavigator.tsx b/ts/navigation/CheckEmailNavigator.tsx index 87d0962c4ea..2a3e0866b92 100644 --- a/ts/navigation/CheckEmailNavigator.tsx +++ b/ts/navigation/CheckEmailNavigator.tsx @@ -9,21 +9,22 @@ const Stack = createStackNavigator(); /** * The onboarding related stack of screens of the application. */ -const navigator = () => ( +const CheckEmailNavigator = () => ( ); -export default navigator; +export default CheckEmailNavigator; diff --git a/ts/navigation/NavigationService.ts b/ts/navigation/NavigationService.ts index 3679e48dd66..7ac82487eae 100644 --- a/ts/navigation/NavigationService.ts +++ b/ts/navigation/NavigationService.ts @@ -5,8 +5,10 @@ import { import { Route } from "@react-navigation/routers"; import React from "react"; import { mixpanelTrack } from "../mixpanel"; +import { AppParamsList } from "./params/AppParamsList"; -export const navigationRef = React.createRef(); +export const navigationRef = + React.createRef>(); // eslint-disable-next-line functional/no-let let isNavigationReady: boolean = false; @@ -37,13 +39,27 @@ const withLogging = }; // NavigationContainerComponent -const getNavigator = (): React.RefObject => - navigationRef; +const getNavigator = (): React.RefObject< + NavigationContainerRef +> => navigationRef; // NavigationParams -const navigate = (routeName: string, params?: any) => { +// This definition comes from react-navigation navigate definition. +type NavigationParams = T extends unknown + ? // This condition checks if the params are optional, + // which means it's either undefined or a union with undefined + undefined extends AppParamsList[T] + ? + | [screen: T] // if the params are optional, we don't have to provide it + | [screen: T, params: AppParamsList[T]] + : [screen: T, params: AppParamsList[T]] + : never; + +const navigate = ( + ...args: NavigationParams +) => { if (isNavigationReady) { - navigationRef.current?.navigate(routeName, params); + navigationRef.current?.navigate(...args); } }; diff --git a/ts/navigation/NotAuthenticatedStackNavigator.tsx b/ts/navigation/NotAuthenticatedStackNavigator.tsx index 59b6338b717..8df6174f0d0 100644 --- a/ts/navigation/NotAuthenticatedStackNavigator.tsx +++ b/ts/navigation/NotAuthenticatedStackNavigator.tsx @@ -13,8 +13,7 @@ const Stack = createStackNavigator(); const NotAuthenticatedStackNavigator = () => ( (); /** * The onboarding related stack of screens of the application. */ -const OnboardingNavigator = () => { - const isEmailUniquenessValidationEnabled = useIOSelector( - isEmailUniquenessValidationEnabledSelector - ); - return ( - - - - - - - - - - - {isEmailUniquenessValidationEnabled ? ( - - ) : ( - - )} - - - - - ); -}; +const OnboardingNavigator = () => ( + + + + + + + + + + + + + + + + +); export default OnboardingNavigator; diff --git a/ts/navigation/ProfileNavigator.tsx b/ts/navigation/ProfileNavigator.tsx index 2775df01116..4570f5208d6 100644 --- a/ts/navigation/ProfileNavigator.tsx +++ b/ts/navigation/ProfileNavigator.tsx @@ -3,15 +3,11 @@ import * as React from "react"; import LogoutScreen from "../components/screens/LogoutScreen"; import { remindersOptInEnabled } from "../config"; import { DesignSystemNavigator } from "../features/design-system/navigation/navigator"; -import { isEmailUniquenessValidationEnabledSelector } from "../features/fastLogin/store/selectors"; import LollipopPlayground from "../features/lollipop/playgrounds/LollipopPlayground"; import CalendarsPreferencesScreen from "../screens/profile/CalendarsPreferencesScreen"; -import CduEmailInsertScreen from "../screens/profile/CduEmailInsertScreen"; import CgnLandingPlayground from "../screens/profile/CgnLandingPlayground"; import DownloadProfileDataScreen from "../screens/profile/DownloadProfileDataScreen"; import EmailForwardingScreen from "../screens/profile/EmailForwardingScreen"; -import EmailInsertScreen from "../screens/profile/EmailInsertScreen"; -import EmailReadScreen from "../screens/profile/EmailReadScreen"; import FiscalCodeScreen from "../screens/profile/FiscalCodeScreen"; import LanguagesPreferencesScreen from "../screens/profile/LanguagesPreferencesScreen"; import { NotificationsPreferencesScreen } from "../screens/profile/NotificationsPreferencesScreen"; @@ -31,10 +27,11 @@ import WebPlayground from "../screens/profile/WebPlayground"; import { IdPayCodePlayGround } from "../screens/profile/playgrounds/IdPayCodePlayground"; import IdPayOnboardingPlayground from "../screens/profile/playgrounds/IdPayOnboardingPlayground"; import MarkdownPlayground from "../screens/profile/playgrounds/MarkdownPlayground"; +import { WalletPaymentPlayground } from "../screens/profile/playgrounds/WalletPaymentPlayground"; import WalletPlayground from "../screens/profile/playgrounds/WalletPlayground"; -import { useIOSelector } from "../store/hooks"; import { isGestureEnabled } from "../utils/navigation"; -import { WalletPaymentPlayground } from "../screens/profile/playgrounds/WalletPaymentPlayground"; +import EmailValidationSendEmailScreen from "../screens/profile/EmailValidationSendEmailScreen"; +import EmailInsertScreen from "../screens/profile/EmailInsertScreen"; import { ProfileParamsList } from "./params/ProfileParamsList"; import ROUTES from "./routes"; @@ -43,193 +40,145 @@ const Stack = createStackNavigator(); /** * A navigator for all the screens of the Profile section */ -const ProfileStackNavigator = () => { - const isEmailUniquenessValidationEnabled = useIOSelector( - isEmailUniquenessValidationEnabledSelector - ); - - return ( - - - - - - - - - - - - - - - - {isEmailUniquenessValidationEnabled ? ( - - ) : ( - - )} - - - - - - - - - - - - - - - {remindersOptInEnabled && ( - - )} - - ); -}; +const ProfileStackNavigator = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {remindersOptInEnabled && ( + + )} + +); export default ProfileStackNavigator; diff --git a/ts/navigation/ServicesHomeTabNavigator.tsx b/ts/navigation/ServicesHomeTabNavigator.tsx index 34a4e1bab1f..388a1a36613 100644 --- a/ts/navigation/ServicesHomeTabNavigator.tsx +++ b/ts/navigation/ServicesHomeTabNavigator.tsx @@ -12,18 +12,17 @@ const Tab = createMaterialTopTabNavigator(); const ServicesHomeTabNavigator = () => ( ( height: 34 } }} + tabBarPosition="top" > ( - - - {myPortalEnabled && ( - - )} - -); - -export default ServicesNavigator; diff --git a/ts/navigation/TabNavigator.tsx b/ts/navigation/TabNavigator.tsx index 864b22d6e21..73cb8af02ed 100644 --- a/ts/navigation/TabNavigator.tsx +++ b/ts/navigation/TabNavigator.tsx @@ -1,23 +1,28 @@ +import { IOColors } from "@pagopa/io-app-design-system"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import { useNavigation } from "@react-navigation/native"; import * as React from "react"; import { StyleSheet } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { IOColors } from "@pagopa/io-app-design-system"; -import { makeFontStyleObject } from "../components/core/fonts"; import LoadingSpinnerOverlay from "../components/LoadingSpinnerOverlay"; +import { makeFontStyleObject } from "../components/core/fonts"; import { TabIconComponent } from "../components/ui/TabIconComponent"; -import I18n from "../i18n"; import MessagesHomeScreen from "../features/messages/screens/MessagesHomeScreen"; +import { WalletHomeScreen as NewWalletHomeScreen } from "../features/newWallet/screens/WalletHomeScreen"; +import { PaymentsHomeScreen } from "../features/payments/home/screens/PaymentsHomeScreen"; +import I18n from "../i18n"; import ProfileMainScreen from "../screens/profile/ProfileMainScreen"; import ServicesHomeScreen from "../screens/services/ServicesHomeScreen"; import WalletHomeScreen from "../screens/wallet/WalletHomeScreen"; import { useIOSelector } from "../store/hooks"; -import { isDesignSystemEnabledSelector } from "../store/reducers/persistedPreferences"; +import { + isDesignSystemEnabledSelector, + isNewWalletSectionEnabledSelector +} from "../store/reducers/persistedPreferences"; import { StartupStatusEnum, isStartupLoaded } from "../store/reducers/startup"; import variables from "../theme/variables"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; -import { AppParamsList, IOStackNavigationProp } from "./params/AppParamsList"; +import { SERVICES_ROUTES } from "../features/services/navigation/routes"; +import { useIONavigation } from "./params/AppParamsList"; import { MainTabParamsList } from "./params/MainTabParamsList"; import ROUTES from "./routes"; import { HeaderFirstLevelHandler } from "./components/HeaderFirstLevelHandler"; @@ -46,13 +51,19 @@ const styles = StyleSheet.create({ }); export const MainTabNavigator = () => { + const navigation = useIONavigation(); const insets = useSafeAreaInsets(); + const startupLoaded = useIOSelector(isStartupLoaded); + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); + const showBarcodeScanSection = false; // Currently disabled + const tabBarHeight = 54; const additionalPadding = 10; const bottomInset = insets.bottom === 0 ? additionalPadding : insets.bottom; - const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); - const navigation = useNavigation>(); const navigateToBarcodeScanScreen = () => { navigation.navigate(ROUTES.BARCODE_SCAN); @@ -63,10 +74,14 @@ export const MainTabNavigator = () => { isLoading={startupLoaded === StartupStatusEnum.ONBOARDING} loadingOpacity={1} > - ( + + ), + tabBarLabelStyle: { fontSize: isDesignSystemEnabled ? 10 : 12, ...makeFontStyleObject( "Regular", @@ -74,13 +89,13 @@ export const MainTabNavigator = () => { isDesignSystemEnabled ? "ReadexPro" : "TitilliumWeb" ) }, - keyboardHidesTabBar: true, - allowFontScaling: false, - activeTintColor: isDesignSystemEnabled + tabBarHideOnKeyboard: true, + tabBarAllowFontScaling: false, + tabBarActiveTintColor: isDesignSystemEnabled ? IOColors["blueIO-500"] : IOColors.blue, - inactiveTintColor: IOColors["grey-850"], - style: [ + tabBarInactiveTintColor: IOColors["grey-850"], + tabBarStyle: [ styles.tabBarStyle, { height: tabBarHeight + bottomInset }, insets.bottom === 0 ? { paddingBottom: additionalPadding } : {} @@ -106,7 +121,9 @@ export const MainTabNavigator = () => { /> ( @@ -119,7 +136,7 @@ export const MainTabNavigator = () => { ) }} /> - {isDesignSystemEnabled && ( + {showBarcodeScanSection && ( { }} /> )} + {isNewWalletSectionEnabled && ( + ( + + ) + }} + /> + )} ( const WalletNavigator = () => ( { + const navigation = useNavigation(); + + return ( + + { + navigation.goBack(); + }} + accessibilityLabel={I18n.t("global.buttons.close")} + /> + + ); +}; +export default CloseButton; diff --git a/ts/navigation/components/HeaderFirstLevelHandler.tsx b/ts/navigation/components/HeaderFirstLevelHandler.tsx index 4e6186f6aaf..c4dec86c05c 100644 --- a/ts/navigation/components/HeaderFirstLevelHandler.tsx +++ b/ts/navigation/components/HeaderFirstLevelHandler.tsx @@ -1,12 +1,9 @@ -/* eslint-disable functional/immutable-data */ -import React, { ComponentProps, useEffect, useMemo } from "react"; import { ActionProp, HeaderFirstLevel } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { useIODispatch, useIOSelector } from "../../store/hooks"; -import { MainTabParamsList } from "../params/MainTabParamsList"; +import React, { ComponentProps, useMemo } from "react"; import { useWalletHomeHeaderBottomSheet } from "../../components/wallet/WalletHomeHeader"; +import { MESSAGES_ROUTES } from "../../features/messages/navigation/routes"; import { SupportRequestParams, useStartSupportRequest @@ -14,54 +11,46 @@ import { import I18n from "../../i18n"; import { navigateToServicePreferenceScreen } from "../../store/actions/navigation"; import { searchMessagesEnabled } from "../../store/actions/search"; -import { currentRouteSelector } from "../../store/reducers/navigation"; +import { useIODispatch, useIOSelector } from "../../store/hooks"; +import { isNewWalletSectionEnabledSelector } from "../../store/reducers/persistedPreferences"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; +import { MainTabParamsList } from "../params/MainTabParamsList"; +import ROUTES from "../routes"; type HeaderFirstLevelProps = ComponentProps; -type TabRoutes = - | Exclude - | "MESSAGES_INBOX" - | "MESSAGES_ARCHIVE" - | "SERVICES_NATIONAL" - | "SERVICES_LOCAL"; +type TabRoutes = keyof MainTabParamsList; const headerHelpByRoute: Record = { - BARCODE_SCAN: {}, - MESSAGES_ARCHIVE: { - faqCategories: ["messages"], - contextualHelpMarkdown: { - title: "messages.contextualHelpTitle", - body: "messages.contextualHelpContent" - } - }, - MESSAGES_INBOX: { + [MESSAGES_ROUTES.MESSAGES_HOME]: { faqCategories: ["messages"], contextualHelpMarkdown: { title: "messages.contextualHelpTitle", body: "messages.contextualHelpContent" } }, - PROFILE_MAIN: { + [ROUTES.PROFILE_MAIN]: { faqCategories: ["profile"], contextualHelpMarkdown: { title: "profile.main.contextualHelpTitle", body: "profile.main.contextualHelpContent" } }, - SERVICES_NATIONAL: { + [SERVICES_ROUTES.SERVICES_HOME]: { faqCategories: ["services"], contextualHelpMarkdown: { title: "services.contextualHelpTitle", body: "services.contextualHelpContent" } }, - SERVICES_LOCAL: { - faqCategories: ["services"], + [ROUTES.WALLET_HOME]: { + faqCategories: ["wallet", "wallet_methods"], contextualHelpMarkdown: { - title: "services.contextualHelpTitle", - body: "services.contextualHelpContent" + title: "wallet.contextualHelpTitle", + body: "wallet.contextualHelpContent" } }, - WALLET_HOME: { + [ROUTES.BARCODE_SCAN]: {}, + [ROUTES.PAYMENTS_HOME]: { faqCategories: ["wallet", "wallet_methods"], contextualHelpMarkdown: { title: "wallet.contextualHelpTitle", @@ -70,17 +59,22 @@ const headerHelpByRoute: Record = { } }; +type Props = { + currentRouteName: TabRoutes; +}; /** * This Component aims to handle the header of the first level screens. based on the current route * it will set the header title and the contextual help and the actions related to the screen * THIS COMPONENT IS NOT MEANT TO BE USED OUTSIDE THE NAVIGATION. * THIS COMPONENT WILL BE REMOVED ONCE REACT NAVIGATION WILL BE UPGRADED TO V6 */ -export const HeaderFirstLevelHandler = () => { +export const HeaderFirstLevelHandler = ({ currentRouteName }: Props) => { const dispatch = useIODispatch(); - const navigation = useNavigation(); - const currentRouteName = useIOSelector(currentRouteSelector); - // console.log("currentRouteName", currentRouteName); + + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); + const requestParams = useMemo( () => pipe( @@ -101,29 +95,16 @@ export const HeaderFirstLevelHandler = () => { }), [startSupportRequest] ); - const headerPropsRef = React.useRef({ - title: I18n.t("messages.contentTitle"), - type: "twoActions", - firstAction: helpAction, - secondAction: { - icon: "search", - accessibilityLabel: I18n.t("global.accessibility.search"), - onPress: () => { - dispatch(searchMessagesEnabled(true)); - } - } - }); const { bottomSheet: WalletHomeHeaderBottomSheet, present: presentWalletHomeHeaderBottomsheet } = useWalletHomeHeaderBottomSheet(); - useEffect(() => { + const headerProps: HeaderFirstLevelProps = useMemo(() => { switch (currentRouteName) { - case "SERVICES_NATIONAL": - case "SERVICES_LOCAL": - headerPropsRef.current = { + case SERVICES_ROUTES.SERVICES_HOME: + return { title: I18n.t("services.title"), type: "twoActions", firstAction: helpAction, @@ -135,33 +116,23 @@ export const HeaderFirstLevelHandler = () => { } } }; - break; - case "PROFILE_MAIN": - headerPropsRef.current = { + case ROUTES.PROFILE_MAIN: + return { title: I18n.t("profile.main.title"), backgroundColor: "dark", type: "singleAction", firstAction: helpAction }; - break; - case "MESSAGES_INBOX": - case "MESSAGES_ARCHIVE": - headerPropsRef.current = { - title: I18n.t("messages.contentTitle"), - type: "twoActions", - firstAction: helpAction, - secondAction: { - icon: "search", - accessibilityLabel: I18n.t("global.accessibility.search"), - onPress: () => { - dispatch(searchMessagesEnabled(true)); - } - } - }; - break; - case "BARCODE_SCAN": - case "WALLET_HOME": - headerPropsRef.current = { + case ROUTES.WALLET_HOME: + if (isNewWalletSectionEnabled) { + return { + title: I18n.t("wallet.wallet"), + type: "singleAction", + firstAction: helpAction, + testID: "wallet-home-header-title" + }; + } + return { title: I18n.t("wallet.wallet"), type: "twoActions", firstAction: helpAction, @@ -174,20 +145,39 @@ export const HeaderFirstLevelHandler = () => { testID: "walletAddNewPaymentMethodTestId" } }; - break; + case ROUTES.PAYMENTS_HOME: + return { + title: "Pagamenti", + type: "singleAction", + firstAction: helpAction + }; + case MESSAGES_ROUTES.MESSAGES_HOME: default: - break; + return { + title: I18n.t("messages.contentTitle"), + type: "twoActions", + firstAction: helpAction, + secondAction: { + icon: "search", + accessibilityLabel: I18n.t("global.accessibility.search"), + onPress: () => { + dispatch(searchMessagesEnabled(true)); + } + } + }; } - navigation.setOptions({ - header: () => - }); }, [ - navigation, currentRouteName, helpAction, presentWalletHomeHeaderBottomsheet, + isNewWalletSectionEnabled, dispatch ]); - return <>{WalletHomeHeaderBottomSheet}; + return ( + <> + + {WalletHomeHeaderBottomSheet} + + ); }; diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index 6e234b954b6..1075af55c36 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -1,7 +1,8 @@ import { NavigatorScreenParams, ParamListBase, - RouteProp + RouteProp, + useNavigation } from "@react-navigation/native"; import { StackNavigationProp } from "@react-navigation/stack"; import { CdcBonusRequestParamsList } from "../../features/bonus/cdc/navigation/params"; @@ -41,36 +42,33 @@ import { IDPayUnsubscriptionParamsList, IDPayUnsubscriptionRoutes } from "../../features/idpay/unsubscription/navigation/navigator"; +import { MessagesParamsList } from "../../features/messages/navigation/params"; +import { MESSAGES_ROUTES } from "../../features/messages/navigation/routes"; +import { WalletParamsList as NewWalletParamsList } from "../../features/newWallet/navigation/params"; +import { WalletRoutes as NewWalletRoutes } from "../../features/newWallet/navigation/routes"; +import { PaymentsBarcodeParamsList } from "../../features/payments/barcode/navigation/params"; +import { PaymentsBarcodeRoutes } from "../../features/payments/barcode/navigation/routes"; +import { PaymentsCheckoutParamsList } from "../../features/payments/checkout/navigation/params"; +import { PaymentsCheckoutRoutes } from "../../features/payments/checkout/navigation/routes"; +import { PaymentsMethodDetailsParamsList } from "../../features/payments/details/navigation/params"; +import { PaymentsMethodDetailsRoutes } from "../../features/payments/details/navigation/routes"; +import { PaymentsOnboardingParamsList } from "../../features/payments/onboarding/navigation/params"; +import { PaymentsOnboardingRoutes } from "../../features/payments/onboarding/navigation/routes"; +import { PaymentsTransactionParamsList } from "../../features/payments/transaction/navigation/params"; +import { PaymentsTransactionRoutes } from "../../features/payments/transaction/navigation/routes"; +import { ServicesParamsList } from "../../features/services/navigation/params"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; import UADONATION_ROUTES from "../../features/uaDonations/navigation/routes"; import { UAWebviewScreenNavigationParams } from "../../features/uaDonations/screens/UAWebViewScreen"; -import { WalletBarcodeParamsList } from "../../features/walletV3/barcode/navigation/params"; -import { WalletBarcodeRoutes } from "../../features/walletV3/barcode/navigation/routes"; -import { - WalletOnboardingParamsList, - WalletOnboardingRoutes -} from "../../features/walletV3/onboarding/navigation/navigator"; -import { - WalletDetailsParamsList, - WalletDetailsRoutes -} from "../../features/walletV3/details/navigation/navigator"; -import { - WalletTransactionParamsList, - WalletTransactionRoutes -} from "../../features/walletV3/transaction/navigation/navigator"; -import { WalletPaymentParamsList } from "../../features/walletV3/payment/navigation/params"; -import { WalletPaymentRoutes } from "../../features/walletV3/payment/navigation/routes"; import { ZendeskParamsList } from "../../features/zendesk/navigation/params"; import ZENDESK_ROUTES from "../../features/zendesk/navigation/routes"; import ROUTES from "../routes"; -import { MESSAGES_ROUTES } from "../../features/messages/navigation/routes"; -import { MessagesParamsList } from "../../features/messages/navigation/params"; import { AuthenticationParamsList } from "./AuthenticationParamsList"; +import { CheckEmailParamsList } from "./CheckEmailParamsList"; import { MainTabParamsList } from "./MainTabParamsList"; import { OnboardingParamsList } from "./OnboardingParamsList"; import { ProfileParamsList } from "./ProfileParamsList"; -import { ServicesParamsList } from "./ServicesParamsList"; import { WalletParamsList } from "./WalletParamsList"; -import { CheckEmailParamsList } from "./CheckEmailParamsList"; export type AppParamsList = { [ROUTES.INGRESS]: undefined; @@ -83,7 +81,7 @@ export type AppParamsList = { [MESSAGES_ROUTES.MESSAGES_NAVIGATOR]: NavigatorScreenParams; [ROUTES.WALLET_NAVIGATOR]: NavigatorScreenParams; - [ROUTES.SERVICES_NAVIGATOR]: NavigatorScreenParams; + [SERVICES_ROUTES.SERVICES_NAVIGATOR]: NavigatorScreenParams; [ROUTES.PROFILE_NAVIGATOR]: NavigatorScreenParams; [ROUTES.BARCODE_SCAN]: undefined; @@ -114,11 +112,13 @@ export type AppParamsList = { [IdPayBarcodeRoutes.IDPAY_BARCODE_MAIN]: NavigatorScreenParams; - [WalletOnboardingRoutes.WALLET_ONBOARDING_MAIN]: NavigatorScreenParams; - [WalletPaymentRoutes.WALLET_PAYMENT_MAIN]: NavigatorScreenParams; - [WalletBarcodeRoutes.WALLET_BARCODE_MAIN]: NavigatorScreenParams; - [WalletDetailsRoutes.WALLET_DETAILS_MAIN]: NavigatorScreenParams; - [WalletTransactionRoutes.WALLET_TRANSACTION_MAIN]: NavigatorScreenParams; + [NewWalletRoutes.WALLET_NAVIGATOR]: NavigatorScreenParams; + + [PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR]: NavigatorScreenParams; + [PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR]: NavigatorScreenParams; + [PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR]: NavigatorScreenParams; + [PaymentsMethodDetailsRoutes.PAYMENT_METHOD_DETAILS_NAVIGATOR]: NavigatorScreenParams; + [PaymentsTransactionRoutes.PAYMENT_TRANSACTION_NAVIGATOR]: NavigatorScreenParams; }; /** @@ -138,3 +138,6 @@ export type IOStackNavigationProp< ParamList extends ParamListBase, RouteName extends keyof ParamList = string > = StackNavigationProp; + +export const useIONavigation = () => + useNavigation>(); diff --git a/ts/navigation/params/AuthenticationParamsList.ts b/ts/navigation/params/AuthenticationParamsList.ts index a57dc1f97f6..ad3e052bdbf 100644 --- a/ts/navigation/params/AuthenticationParamsList.ts +++ b/ts/navigation/params/AuthenticationParamsList.ts @@ -6,13 +6,14 @@ import ROUTES from "../routes"; export type AuthenticationParamsList = { [ROUTES.AUTHENTICATION_LANDING]: undefined; + [ROUTES.AUTHENTICATION_ROOTED_DEVICE]: undefined; [ROUTES.AUTHENTICATION_OPT_IN]: ChosenIdentifier; [ROUTES.AUTHENTICATION_IDP_SELECTION]: undefined; [ROUTES.AUTHENTICATION_CIE]: undefined; [ROUTES.AUTHENTICATION_IDP_LOGIN]: undefined; [ROUTES.AUTHENTICATION_AUTH_SESSION]: undefined; [ROUTES.AUTHENTICATION_IDP_TEST]: undefined; - [ROUTES.MARKDOWN]: undefined; + [ROUTES.CIE_NOT_SUPPORTED]: undefined; // For expired cie screen [ROUTES.CIE_EXPIRED_SCREEN]: undefined; [ROUTES.CIE_PIN_SCREEN]: undefined; diff --git a/ts/navigation/params/MainTabParamsList.ts b/ts/navigation/params/MainTabParamsList.ts index 64ac4448bfe..0a63d9f4937 100644 --- a/ts/navigation/params/MainTabParamsList.ts +++ b/ts/navigation/params/MainTabParamsList.ts @@ -1,11 +1,13 @@ import { WalletHomeNavigationParams } from "../../screens/wallet/WalletHomeScreen"; import ROUTES from "../routes"; import { MESSAGES_ROUTES } from "../../features/messages/navigation/routes"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; export type MainTabParamsList = { [MESSAGES_ROUTES.MESSAGES_HOME]: undefined; [ROUTES.WALLET_HOME]: WalletHomeNavigationParams; [ROUTES.BARCODE_SCAN]: undefined; - [ROUTES.SERVICES_HOME]: undefined; + [SERVICES_ROUTES.SERVICES_HOME]: undefined; + [ROUTES.PAYMENTS_HOME]: undefined; [ROUTES.PROFILE_MAIN]: undefined; }; diff --git a/ts/navigation/params/OnboardingParamsList.ts b/ts/navigation/params/OnboardingParamsList.ts index b4ff0b6a2b2..dada8c7e052 100644 --- a/ts/navigation/params/OnboardingParamsList.ts +++ b/ts/navigation/params/OnboardingParamsList.ts @@ -1,6 +1,7 @@ import { OnboardingNotificationsPreferencesScreenNavigationParams } from "../../screens/onboarding/OnboardingNotificationsPreferencesScreen"; import { OnboardingServicesPreferenceScreenNavigationParams } from "../../screens/onboarding/OnboardingServicesPreferenceScreen"; -import { CduEmailInsertScreenNavigationParams } from "../../screens/profile/CduEmailInsertScreen"; +import { EmailInsertScreenNavigationParams } from "../../screens/profile/EmailInsertScreen"; +import { SendEmailValidationScreenProp } from "../../screens/profile/EmailValidationSendEmailScreen"; import ROUTES from "../routes"; export type OnboardingParamsList = { @@ -12,8 +13,8 @@ export type OnboardingParamsList = { [ROUTES.ONBOARDING_MISSING_DEVICE_PIN]: undefined; [ROUTES.ONBOARDING_MISSING_DEVICE_BIOMETRIC]: undefined; [ROUTES.ONBOARDING_FINGERPRINT]: undefined; - [ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN]: undefined; - [ROUTES.ONBOARDING_READ_EMAIL_SCREEN]: CduEmailInsertScreenNavigationParams; + [ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN]: EmailInsertScreenNavigationParams; + [ROUTES.ONBOARDING_EMAIL_VERIFICATION_SCREEN]: SendEmailValidationScreenProp; [ROUTES.ONBOARDING_COMPLETED]: undefined; [ROUTES.ONBOARDING_NOTIFICATIONS_PREFERENCES]: OnboardingNotificationsPreferencesScreenNavigationParams; [ROUTES.ONBOARDING_NOTIFICATIONS_INFO_SCREEN_CONSENT]: undefined; diff --git a/ts/navigation/params/ProfileParamsList.ts b/ts/navigation/params/ProfileParamsList.ts index 43329d0f027..78689e58249 100644 --- a/ts/navigation/params/ProfileParamsList.ts +++ b/ts/navigation/params/ProfileParamsList.ts @@ -1,4 +1,5 @@ -import { CduEmailInsertScreenNavigationParams } from "../../screens/profile/CduEmailInsertScreen"; +import { EmailInsertScreenNavigationParams } from "../../screens/profile/EmailInsertScreen"; +import { SendEmailValidationScreenProp } from "../../screens/profile/EmailValidationSendEmailScreen"; import ROUTES from "../routes"; export type ProfileParamsList = { @@ -15,8 +16,8 @@ export type ProfileParamsList = { [ROUTES.PROFILE_ABOUT_APP]: undefined; [ROUTES.PROFILE_LOGOUT]: undefined; [ROUTES.PROFILE_FISCAL_CODE]: undefined; - [ROUTES.READ_EMAIL_SCREEN]: undefined; - [ROUTES.INSERT_EMAIL_SCREEN]: CduEmailInsertScreenNavigationParams; + [ROUTES.INSERT_EMAIL_SCREEN]: EmailInsertScreenNavigationParams; + [ROUTES.EMAIL_VERIFICATION_SCREEN]: SendEmailValidationScreenProp; [ROUTES.PIN_SCREEN]: undefined; [ROUTES.PROFILE_DOWNLOAD_DATA]: undefined; [ROUTES.MARKDOWN_PLAYGROUND]: undefined; diff --git a/ts/navigation/params/ServicesParamsList.ts b/ts/navigation/params/ServicesParamsList.ts deleted file mode 100644 index 66253c09d4e..00000000000 --- a/ts/navigation/params/ServicesParamsList.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ServiceDetailsScreenNavigationParams } from "../../screens/services/ServiceDetailsScreen"; -import { ServiceWebviewScreenNavigationParams } from "../../screens/services/ServicesWebviewScreen"; -import ROUTES from "../routes"; - -export type ServicesParamsList = { - [ROUTES.SERVICE_DETAIL]: ServiceDetailsScreenNavigationParams; - [ROUTES.SERVICE_WEBVIEW]: ServiceWebviewScreenNavigationParams; -}; diff --git a/ts/navigation/params/WalletParamsList.ts b/ts/navigation/params/WalletParamsList.ts index fec5a2ce8a4..5c2d8884144 100644 --- a/ts/navigation/params/WalletParamsList.ts +++ b/ts/navigation/params/WalletParamsList.ts @@ -29,6 +29,7 @@ import ROUTES from "../routes"; import { BONUS_ROUTES } from "../../features/bonus/common/navigation/navigator"; export type WalletParamsList = { + [ROUTES.WALLET_HOME]: undefined; [ROUTES.WALLET_IDPAY_INITIATIVE_LIST]: IdPayInstrumentInitiativesScreenRouteParams; [ROUTES.WALLET_ADD_PAYMENT_METHOD]: AddPaymentMethodScreenNavigationParams; [ROUTES.WALLET_TRANSACTION_DETAILS]: TransactionDetailsScreenNavigationParams; diff --git a/ts/navigation/routes.ts b/ts/navigation/routes.ts index 7f2d8ccd385..9e314922dad 100644 --- a/ts/navigation/routes.ts +++ b/ts/navigation/routes.ts @@ -8,12 +8,14 @@ const ROUTES = { // Authentication AUTHENTICATION: "AUTHENTICATION", AUTHENTICATION_LANDING: "AUTHENTICATION_LANDING", + AUTHENTICATION_ROOTED_DEVICE: "AUTHENTICATION_ROOTED_DEVICE", AUTHENTICATION_OPT_IN: "AUTHENTICATION_OPT_IN", AUTHENTICATION_IDP_SELECTION: "AUTHENTICATION_IDP_SELECTION", AUTHENTICATION_IDP_LOGIN: "AUTHENTICATION_IDP_LOGIN", AUTHENTICATION_AUTH_SESSION: "AUTHENTICATION_AUTH_SESSION", AUTHENTICATION_IDP_TEST: "AUTHENTICATION_IDP_TEST", AUTHENTICATION_CIE: "AUTHENTICATION_CIE", + CIE_NOT_SUPPORTED: "CIE_NOT_SUPPORTED", MARKDOWN: "MARKDOWN", // CIE @@ -46,8 +48,8 @@ const ROUTES = { ONBOARDING_NOTIFICATIONS_PREFERENCES: "ONBOARDING_NOTIFICATIONS_PREFERENCES", ONBOARDING_NOTIFICATIONS_INFO_SCREEN_CONSENT: "ONBOARDING_NOTIFICATIONS_INFO_SCREEN_CONSENT", - ONBOARDING_READ_EMAIL_SCREEN: "ONBOARDING_READ_EMAIL_SCREEN", ONBOARDING_INSERT_EMAIL_SCREEN: "ONBOARDING_INSERT_EMAIL_SCREEN", + ONBOARDING_EMAIL_VERIFICATION_SCREEN: "ONBOARDING_EMAIL_VERIFICATION_SCREEN", // Wallet WALLET_NAVIGATOR: "WALLET_NAVIGATOR", @@ -67,6 +69,7 @@ const ROUTES = { ADD_CREDIT_CARD_OUTCOMECODE_MESSAGE: "ADD_CREDIT_CARD_OUTCOMECODE_MESSAGE", // Payment + PAYMENTS_HOME: "PAYMENTS_HOME", PAYMENT_SCAN_QR_CODE: "PAYMENT_SCAN_QR_CODE", PAYMENT_MANUAL_DATA_INSERTION: "PAYMENT_MANUAL_DATA_INSERTION", PAYMENT_TRANSACTION_SUMMARY: "PAYMENT_TRANSACTION_SUMMARY", @@ -85,12 +88,6 @@ const ROUTES = { // Main MAIN: "MAIN", - // Services - SERVICES_NAVIGATOR: "SERVICES_NAVIGATOR", - SERVICES_HOME: "SERVICES_HOME", - SERVICE_DETAIL: "SERVICE_DETAIL", - SERVICE_WEBVIEW: "SERVICE_WEBVIEW", - // Profile PROFILE_NAVIGATOR: "PROFILE_NAVIGATOR", PROFILE_MAIN: "PROFILE_MAIN", @@ -131,8 +128,8 @@ const ROUTES = { IDPAY_CODE_PLAYGROUND: "IDPAY_CODE_PLAYGROUND", // Preferences - READ_EMAIL_SCREEN: "READ_EMAIL_SCREEN", INSERT_EMAIL_SCREEN: "INSERT_EMAIL_SCREEN", + EMAIL_VERIFICATION_SCREEN: "EMAIL_VERIFICATION_SCREEN", PIN_SCREEN: "PIN_SCREEN", // Used when the App is in background diff --git a/ts/sagas/__tests__/notifications.test.ts b/ts/sagas/__tests__/notifications.test.ts index 24fa73d2c61..419e193b61a 100644 --- a/ts/sagas/__tests__/notifications.test.ts +++ b/ts/sagas/__tests__/notifications.test.ts @@ -314,7 +314,7 @@ describe("trackMessageNotificationTapIfNeeded", () => { it("should call trackMessageNotificationTap when there is a PendingMessageState that requires tracking", () => { const spiedTrackMessageNotificationTap = jest .spyOn(Analytics, "trackMessageNotificationTap") - .mockImplementation(_ => Promise.resolve()); + .mockImplementation(jest.fn()); const mockPendingMessageState = { id: "001", foreground: true, @@ -326,7 +326,7 @@ describe("trackMessageNotificationTapIfNeeded", () => { it("should not call trackMessageNotificationTap when there is a PendingMessageState that does not require tracking", () => { const spiedTrackMessageNotificationTap = jest .spyOn(Analytics, "trackMessageNotificationTap") - .mockImplementation(_ => Promise.resolve()); + .mockImplementation(jest.fn()); const mockPendingMessageState = { id: "001", foreground: true, @@ -338,7 +338,7 @@ describe("trackMessageNotificationTapIfNeeded", () => { it("should not call trackMessageNotificationTap when there is a PendingMessageState that does not have a tracking information", () => { const spiedTrackMessageNotificationTap = jest .spyOn(Analytics, "trackMessageNotificationTap") - .mockImplementation(_ => Promise.resolve()); + .mockImplementation(jest.fn()); const mockPendingMessageState = { id: "001", foreground: true @@ -349,7 +349,7 @@ describe("trackMessageNotificationTapIfNeeded", () => { it("should not call trackMessageNotificationTap when there is not a PendingMessageState", () => { const spiedTrackMessageNotificationTap = jest .spyOn(Analytics, "trackMessageNotificationTap") - .mockImplementation(_ => Promise.resolve()); + .mockImplementation(jest.fn()); trackMessageNotificationTapIfNeeded(); expect(spiedTrackMessageNotificationTap).not.toHaveBeenCalled(); }); diff --git a/ts/sagas/profile.ts b/ts/sagas/profile.ts index c2b708e9a7c..30ed55d7306 100644 --- a/ts/sagas/profile.ts +++ b/ts/sagas/profile.ts @@ -171,9 +171,7 @@ function* createOrUpdateProfileSaga( createOrUpdateProfile({ body: newProfile }), - undefined, - undefined, - true + { skipThrowingError: true } )) as unknown as SagaCallReturnType; if (E.isLeft(response)) { @@ -237,7 +235,7 @@ function* createOrUpdateProfileSaga( const profileChangePredicates: ReadonlyArray< [ (value: InitializedProfile, newValue: InitializedProfile) => boolean, - (value: InitializedProfile) => Promise | undefined + (value: InitializedProfile) => void | undefined ] > = [ [ diff --git a/ts/sagas/services/__tests__/refreshStoredServices.test.ts b/ts/sagas/services/__tests__/refreshStoredServices.test.ts index 340c2466b36..3f47312e979 100644 --- a/ts/sagas/services/__tests__/refreshStoredServices.test.ts +++ b/ts/sagas/services/__tests__/refreshStoredServices.test.ts @@ -11,7 +11,7 @@ import { loadServicesDetail } from "../../../store/actions/services"; import { servicesByIdSelector, ServicesByIdState -} from "../../../store/reducers/entities/services/servicesById"; +} from "../../../features/services/store/reducers/servicesById"; import { refreshStoredServices } from "../refreshStoredServices"; describe("refreshStoredServices", () => { diff --git a/ts/sagas/services/refreshStoredServices.ts b/ts/sagas/services/refreshStoredServices.ts index 726e8e46140..9f3868f301a 100644 --- a/ts/sagas/services/refreshStoredServices.ts +++ b/ts/sagas/services/refreshStoredServices.ts @@ -3,7 +3,7 @@ import { SagaIterator } from "redux-saga"; import { put, select } from "typed-redux-saga/macro"; import { PaginatedServiceTupleCollection } from "../../../definitions/backend/PaginatedServiceTupleCollection"; import { loadServicesDetail } from "../../store/actions/services"; -import { servicesByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { servicesByIdSelector } from "../../features/services/store/reducers/servicesById"; /** * Check which services detail must be loaded. If there are, loading action will be dispatched diff --git a/ts/sagas/services/removeUnusedStoredServices.ts b/ts/sagas/services/removeUnusedStoredServices.ts index 0896df23000..4176faa0cad 100644 --- a/ts/sagas/services/removeUnusedStoredServices.ts +++ b/ts/sagas/services/removeUnusedStoredServices.ts @@ -4,7 +4,7 @@ import { SagaIterator } from "redux-saga"; import { put, select } from "typed-redux-saga/macro"; import { PaginatedServiceTupleCollection } from "../../../definitions/backend/PaginatedServiceTupleCollection"; import { removeServiceTuples } from "../../store/actions/services"; -import { servicesByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { servicesByIdSelector } from "../../features/services/store/reducers/servicesById"; type VisibleServiceVersionById = { [index: string]: number | undefined; diff --git a/ts/sagas/services/watchLoadServicesSaga.ts b/ts/sagas/services/watchLoadServicesSaga.ts index 31c0684c932..f89e5716788 100644 --- a/ts/sagas/services/watchLoadServicesSaga.ts +++ b/ts/sagas/services/watchLoadServicesSaga.ts @@ -2,22 +2,22 @@ import { SagaIterator } from "redux-saga"; import { fork, put, takeEvery, takeLatest } from "typed-redux-saga/macro"; import { getType } from "typesafe-actions"; import { BackendClient } from "../../api/backend"; +import { handleGetServicePreference } from "../../features/services/saga/handleGetServicePreference"; +import { handleUpsertServicePreference } from "../../features/services/saga/handleUpsertServicePreference"; import { loadServiceDetail, loadVisibleServices } from "../../store/actions/services"; +import { + loadServicePreference, + upsertServicePreference +} from "../../features/services/store/actions"; import { loadServiceDetailRequestHandler, watchServicesDetailLoadSaga } from "../startup/loadServiceDetailRequestHandler"; import { loadVisibleServicesRequestHandler } from "../startup/loadVisibleServicesHandler"; -import { - loadServicePreference, - upsertServicePreference -} from "../../store/actions/services/servicePreference"; import { handleFirstVisibleServiceLoadSaga } from "./handleFirstVisibleServiceLoadSaga"; -import { handleGetServicePreference } from "./servicePreference/handleGetServicePreferenceSaga"; -import { handleUpsertServicePreference } from "./servicePreference/handleUpsertServicePreferenceSaga"; /** * A saga for managing requests to load/refresh services data from backend diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index dcfde4a0500..d302d0579e6 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -50,7 +50,7 @@ import { lollipopPublicKeySelector } from "../features/lollipop/store/reducers/l import { watchMessagesSaga } from "../features/messages/saga"; import { handleClearAllAttachments } from "../features/messages/saga/handleClearAttachments"; import { watchPnSaga } from "../features/pn/store/sagas/watchPnSaga"; -import { watchWalletSaga as watchWalletV3Saga } from "../features/walletV3/common/saga"; +import { watchPaymentsSaga } from "../features/payments/common/saga"; import { watchZendeskGetSessionSaga, watchZendeskSupportSaga @@ -64,19 +64,12 @@ import { } from "../store/actions/application"; import { sessionExpired } from "../store/actions/authentication"; import { backendStatusLoadSuccess } from "../store/actions/backendStatus"; -import { - differentProfileLoggedIn, - setProfileHashedFiscalCode -} from "../store/actions/crossSessions"; +import { differentProfileLoggedIn } from "../store/actions/crossSessions"; import { previousInstallationDataDeleteSuccess } from "../store/actions/installation"; import { setMixpanelEnabled } from "../store/actions/mixpanel"; import { navigateToPrivacyScreen } from "../store/actions/navigation"; import { clearOnboarding } from "../store/actions/onboarding"; -import { - clearCache, - profileLoadSuccess, - resetProfileState -} from "../store/actions/profile"; +import { clearCache, resetProfileState } from "../store/actions/profile"; import { startupLoadSuccess } from "../store/actions/startup"; import { loadUserDataProcessing } from "../store/actions/userDataProcessing"; import { @@ -100,7 +93,10 @@ import { StartupStatusEnum } from "../store/reducers/startup"; import { ReduxSagaEffect, SagaCallReturnType } from "../types/utils"; import { trackKeychainGetFailure } from "../utils/analytics"; import { isTestEnv } from "../utils/environment"; +import { walletPaymentHandlersInitialized } from "../store/actions/wallet/payment"; import { deletePin, getPin } from "../utils/keychain"; +import { watchEmailValidationSaga } from "../store/sagas/emailValidationPollingSaga"; +import { handleIsKeyStrongboxBacked } from "../features/lollipop/utils/crypto"; import { clearKeychainError, keychainError @@ -460,13 +456,6 @@ export function* initializeApplicationSaga( } } } - // We dispatch a load success to allow the execution of the check - // which save the hashed code tax code - const profile = yield* select(profileSelector); - if (pot.isSome(profile)) { - yield* put(profileLoadSuccess(profile.value)); - yield* take(setProfileHashedFiscalCode); - } // Ask to accept ToS if there is a new available version yield* call(checkAcceptedTosSaga, userProfile); @@ -482,9 +471,14 @@ export function* initializeApplicationSaga( yield* call(trackKeychainGetFailure, keychainError); yield* call(clearKeychainError); + // track if the Android device has StrongBox + yield* call(handleIsKeyStrongboxBacked, keyInfo.keyTag); + yield* call(checkConfiguredPinSaga); yield* call(checkAcknowledgedFingerprintSaga); + yield* fork(watchEmailValidationSaga); + if (!hasPreviousSessionAndPin || userProfile.email === undefined) { yield* call(checkAcknowledgedEmailSaga, userProfile); } @@ -537,7 +531,7 @@ export function* initializeApplicationSaga( if (pnEnabled) { // Start watching for PN actions - yield* fork(watchPnSaga, sessionToken, backendClient.getVerificaRpt); + yield* fork(watchPnSaga, sessionToken); } const idPayTestEnabled: ReturnType = @@ -549,7 +543,7 @@ export function* initializeApplicationSaga( } // Start watching for Wallet V3 actions - yield* fork(watchWalletV3Saga, maybeSessionInformation.value.walletToken); + yield* fork(watchPaymentsSaga, maybeSessionInformation.value.walletToken); // Load the user metadata yield* call(loadUserMetadata, backendClient.getUserMetadata, true); @@ -637,7 +631,11 @@ export function* initializeApplicationSaga( // This tells the security advice bottomsheet that it can be shown yield* put(setSecurityAdviceReadyToShow(true)); - yield* put(applicationInitialized({ actionsToWaitFor: [] })); + yield* put( + applicationInitialized({ + actionsToWaitFor: [walletPaymentHandlersInitialized] + }) + ); } /** diff --git a/ts/sagas/startup/__tests__/checkProfileEmailSaga.test.tsx b/ts/sagas/startup/__tests__/checkProfileEmailSaga.test.tsx index a4ab1f78e37..56ef936aa20 100644 --- a/ts/sagas/startup/__tests__/checkProfileEmailSaga.test.tsx +++ b/ts/sagas/startup/__tests__/checkProfileEmailSaga.test.tsx @@ -6,7 +6,6 @@ import NavigationService from "../../../navigation/NavigationService"; import ROUTES from "../../../navigation/routes"; import { applicationChangeState } from "../../../store/actions/application"; -import { navigateToEmailReadScreen } from "../../../store/actions/navigation"; import { emailAcknowledged, emailInsert @@ -16,29 +15,12 @@ import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapp import { checkAcknowledgedEmailSaga } from "../checkAcknowledgedEmailSaga"; import { ServicesPreferencesModeEnum } from "../../../../definitions/backend/ServicesPreferencesMode"; -jest.mock("../../../features/fastLogin/store/selectors", () => { - const originalModule = jest.requireActual( - "../../../features/fastLogin/store/selectors" - ); - return { - ...originalModule, - isEmailUniquenessValidationEnabledSelector: () => false - }; -}); - describe("checkAcknowledgedEmailSaga", () => { beforeEach(() => { const globalState = appReducer(undefined, applicationChangeState("active")); const store = createStore(appReducer, globalState as any); renderScreenWithNavigationStoreContext(View, "DUMMY", {}, store); - jest.useRealTimers(); - }); - - describe("when user has an email and it is validated", () => { - it("should do nothing", () => - expectSaga(checkAcknowledgedEmailSaga, mockedProfile) - .not.call(navigateToEmailReadScreen) - .run()); + jest.useFakeTimers(); }); describe("when user is on his first onboarding and he has an email and it is validated", () => { @@ -48,13 +30,14 @@ describe("checkAcknowledgedEmailSaga", () => { mode: ServicesPreferencesModeEnum.LEGACY } }; - it("should show email read screen", async () => { - await expectSaga( + it("should show email read screen", () => { + void expectSaga( checkAcknowledgedEmailSaga, profileEmailValidatedFirstOnboarding ) .call(NavigationService.navigate, ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { isOnboarding: true } }) .run(); }); @@ -65,15 +48,17 @@ describe("checkAcknowledgedEmailSaga", () => { ...mockedProfile, is_email_validated: false }; - it("should prompt the screen to remember to validate", () => - expectSaga(checkAcknowledgedEmailSaga, profileWithEmailNotValidated) + it("should prompt the screen to remember to validate", () => { + void expectSaga(checkAcknowledgedEmailSaga, profileWithEmailNotValidated) // read screen is wrapped in a HOC where if email is validate show ReadScreen // otherwise a screen that remembers to validate it .call(NavigationService.navigate, ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { isOnboarding: true } }) .dispatch(emailAcknowledged()) - .run()); + .run(); + }); }); describe("when user has not an email", () => { @@ -82,16 +67,19 @@ describe("checkAcknowledgedEmailSaga", () => { is_email_validated: false, email: undefined }; - it("should prompt the screen to insert it", async () => { + it("should prompt the screen to insert it", () => { const globalState = appReducer( undefined, applicationChangeState("active") ); const store = createStore(appReducer, globalState as any); renderScreenWithNavigationStoreContext(View, "DUMMY", {}, store); - await expectSaga(checkAcknowledgedEmailSaga, profileWithNoEmail) + void expectSaga(checkAcknowledgedEmailSaga, profileWithNoEmail) .call(NavigationService.navigate, ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { + isOnboarding: true + } }) // go to email insert screen .dispatch(emailInsert()) // dispatch email insert .dispatch(emailAcknowledged()) // press continue diff --git a/ts/sagas/startup/__tests__/loadServiceDetailRequestHandler.test.ts b/ts/sagas/startup/__tests__/loadServiceDetailRequestHandler.test.ts index 5937666cba4..4c573c4ffdc 100644 --- a/ts/sagas/startup/__tests__/loadServiceDetailRequestHandler.test.ts +++ b/ts/sagas/startup/__tests__/loadServiceDetailRequestHandler.test.ts @@ -11,6 +11,7 @@ import { loadServiceDetail } from "../../../store/actions/services"; import { handleOrganizationNameUpdateSaga } from "../../services/handleOrganizationNameUpdateSaga"; import { handleServiceReadabilitySaga } from "../../services/handleServiceReadabilitySaga"; import { loadServiceDetailRequestHandler } from "../loadServiceDetailRequestHandler"; +import { withRefreshApiCall } from "../../../features/fastLogin/saga/utils"; const mockedServiceId = "A01" as ServiceId; const getService = jest.fn(); @@ -32,7 +33,11 @@ describe("loadServiceDetailRequestHandler", () => { it("returns an error if backend response is 500", () => { testSaga(loadServiceDetailRequestHandler, getService, mockedAction) .next() - .call(getService, { service_id: mockedServiceId }) + .call( + withRefreshApiCall, + getService({ service_id: mockedServiceId }), + mockedAction + ) .next(E.right({ status: 500, value: "generic error" })) .put( loadServiceDetail.failure({ @@ -47,7 +52,11 @@ describe("loadServiceDetailRequestHandler", () => { it("returns service detail if the backend response is 200", () => { testSaga(loadServiceDetailRequestHandler, getService, mockedAction) .next() - .call(getService, { service_id: mockedServiceId }) + .call( + withRefreshApiCall, + getService({ service_id: mockedServiceId }), + mockedAction + ) .next(E.right({ status: 200, value: mockedService })) .put(loadServiceDetail.success(mockedService)) .next() diff --git a/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts b/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts index 8eb955f5948..0d659f2f3bf 100644 --- a/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts +++ b/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts @@ -19,7 +19,9 @@ describe("loadVisibleServicesHandler", () => { it("returns a generic error if backend response is 500", () => { testSaga(loadVisibleServicesRequestHandler, getVisibleServices) .next() - .call(withRefreshApiCall, getVisibleServices({})) + .call(withRefreshApiCall, getVisibleServices({}), { + skipThrowingError: true + }) .next( E.right({ status: 500, @@ -39,10 +41,12 @@ describe("loadVisibleServicesHandler", () => { it("the session expiration is handled by withRefreshApiCall", () => { testSaga(loadVisibleServicesRequestHandler, getVisibleServices) .next() - .call(withRefreshApiCall, getVisibleServices({})) + .call(withRefreshApiCall, getVisibleServices({}), { + skipThrowingError: true + }) .next( E.right({ - status: 401, + status: 403, value: "An error occurred loading visible services" }) ) @@ -59,7 +63,9 @@ describe("loadVisibleServicesHandler", () => { it("return an array of visibile services if backend response is 200", () => { testSaga(loadVisibleServicesRequestHandler, getVisibleServices) .next() - .call(withRefreshApiCall, getVisibleServices({})) + .call(withRefreshApiCall, getVisibleServices({}), { + skipThrowingError: true + }) .next(E.right({ status: 200, value: { items: mockedVisibleServices } })) .put(loadVisibleServices.success(mockedVisibleServices)) .next(); diff --git a/ts/sagas/startup/checkAcknowledgedEmailSaga.ts b/ts/sagas/startup/checkAcknowledgedEmailSaga.ts index 519d6b08b1f..0a51e04fff5 100644 --- a/ts/sagas/startup/checkAcknowledgedEmailSaga.ts +++ b/ts/sagas/startup/checkAcknowledgedEmailSaga.ts @@ -1,5 +1,5 @@ -import { call, select, take } from "typed-redux-saga/macro"; -import { StackActions } from "@react-navigation/native"; +import { call, take } from "typed-redux-saga/macro"; +import { CommonActions } from "@react-navigation/native"; import { InitializedProfile } from "../../../definitions/backend/InitializedProfile"; import NavigationService from "../../navigation/NavigationService"; import ROUTES from "../../navigation/routes"; @@ -10,7 +10,6 @@ import { isProfileFirstOnBoarding } from "../../store/reducers/profile"; import { ReduxSagaEffect } from "../../types/utils"; -import { isEmailUniquenessValidationEnabledSelector } from "../../features/fastLogin/store/selectors"; /** * Launch email saga that consists of: @@ -22,24 +21,20 @@ import { isEmailUniquenessValidationEnabledSelector } from "../../features/fastL export function* checkAcknowledgedEmailSaga( userProfile: InitializedProfile ): IterableIterator { - const isEmailUniquenessValidationEnabled = yield* select( - isEmailUniquenessValidationEnabledSelector - ); - // Check if the profile has an email if (hasProfileEmail(userProfile)) { if ( isProfileFirstOnBoarding(userProfile) || - (!isEmailUniquenessValidationEnabled && - !isProfileEmailValidated(userProfile)) + !isProfileEmailValidated(userProfile) ) { // The user profile is just created (first onboarding), the conditional // view displays the screen to show the user's email used in app // OR // An email exists on the user's profile but it is not validated, the conditional - // view shows the component that reminds to validate the email address or allows the navigation to edit it. + // view shows a screen that forces the user to insert and validate his email yield* call(NavigationService.navigate, ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { isOnboarding: true } }); } else { // we can go on, no need to wait @@ -47,13 +42,9 @@ export function* checkAcknowledgedEmailSaga( } } else { // the profile has no email address, user must insert it - // EmailInsertScreen knows if the user comes from onboarding or not - // if he comes from onboarding, on email inserted the navigation will focus EmailReadScreen to remember the user - // to validate it yield* call(NavigationService.navigate, ROUTES.ONBOARDING, { - screen: isEmailUniquenessValidationEnabled - ? ROUTES.ONBOARDING_READ_EMAIL_SCREEN - : ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { isOnboarding: true } }); } @@ -62,6 +53,13 @@ export function* checkAcknowledgedEmailSaga( yield* take(emailAcknowledged); yield* call( NavigationService.dispatchNavigationAction, - StackActions.popToTop() + // We use navigate to go back to the main tab + // https://reactnavigation.org/docs/nesting-navigators/#navigation-actions-are-handled-by-current-navigator-and-bubble-up-if-couldnt-be-handled + CommonActions.navigate({ + name: ROUTES.MAIN, + // If for some reason, we have navigation params + // we want to merge them going back to the main tab. + merge: true + }) ); } diff --git a/ts/sagas/startup/checkEmailSaga.ts b/ts/sagas/startup/checkEmailSaga.ts index 91abb3d17f6..5d9f717c039 100644 --- a/ts/sagas/startup/checkEmailSaga.ts +++ b/ts/sagas/startup/checkEmailSaga.ts @@ -1,7 +1,7 @@ import { call, put, select, take } from "typed-redux-saga/macro"; import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { StackActions } from "@react-navigation/native"; +import { CommonActions } from "@react-navigation/native"; import NavigationService from "../../navigation/NavigationService"; import ROUTES from "../../navigation/routes"; import { @@ -11,33 +11,26 @@ import { } from "../../store/reducers/profile"; import { setEmailCheckAtStartupFailure } from "../../store/actions/profile"; import { emailAcknowledged } from "../../store/actions/onboarding"; -import { isEmailUniquenessValidationEnabledSelector } from "../../features/fastLogin/store/selectors"; export function* checkEmailSaga() { // We get the latest profile from the store const profile = yield* select(profileSelector); - const isEmailUniquenessValidationEnabled = yield* select( - isEmailUniquenessValidationEnabledSelector - ); // When we use this saga, we are sure that the profile is not none if (pot.isSome(profile)) { // eslint-disable-next-line functional/no-let let userProfile = profile.value; - if ( - isEmailUniquenessValidationEnabled && - !isProfileEmailValidated(userProfile) - ) { + if (!isProfileEmailValidated(userProfile)) { yield* put(setEmailCheckAtStartupFailure(O.some(true))); if (isProfileEmailAlreadyTaken(userProfile)) { yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, - params: { email: userProfile.email } + params: { email: userProfile.email ?? "" } }); } else { yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { screen: ROUTES.CHECK_EMAIL_NOT_VERIFIED, - params: { email: userProfile.email } + params: { email: userProfile.email ?? "" } }); } // Wait for the user to press "Continue" button after having checked out @@ -45,9 +38,15 @@ export function* checkEmailSaga() { yield* take(emailAcknowledged); yield* call( NavigationService.dispatchNavigationAction, - StackActions.popToTop() + // We use navigate to go back to the main tab + // https://reactnavigation.org/docs/nesting-navigators/#navigation-actions-are-handled-by-current-navigator-and-bubble-up-if-couldnt-be-handled + CommonActions.navigate({ + name: ROUTES.MAIN, + // If for some reason, we have navigation params + // we want to merge them going back to the main tab. + merge: true + }) ); - // We get the latest profile from the store and return it const maybeUpdatedProfile = yield* select(profileSelector); if (pot.isSome(maybeUpdatedProfile)) { diff --git a/ts/sagas/startup/loadServiceDetailRequestHandler.ts b/ts/sagas/startup/loadServiceDetailRequestHandler.ts index 87403c9f71e..af257fbb660 100644 --- a/ts/sagas/startup/loadServiceDetailRequestHandler.ts +++ b/ts/sagas/startup/loadServiceDetailRequestHandler.ts @@ -2,7 +2,14 @@ import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import { Millisecond } from "@pagopa/ts-commons/lib/units"; import * as E from "fp-ts/lib/Either"; import { buffers, channel, Channel } from "redux-saga"; -import { call, fork, put, take, takeLatest } from "typed-redux-saga/macro"; +import { + call, + fork, + put, + select, + take, + takeLatest +} from "typed-redux-saga/macro"; import { ActionType, getType } from "typesafe-actions"; import { ServiceId } from "../../../definitions/backend/ServiceId"; import { PathTraversalSafePathParam } from "../../../definitions/backend/PathTraversalSafePathParam"; @@ -19,6 +26,8 @@ import { convertUnknownToError } from "../../utils/errors"; import { handleOrganizationNameUpdateSaga } from "../services/handleOrganizationNameUpdateSaga"; import { handleServiceReadabilitySaga } from "../services/handleServiceReadabilitySaga"; import { trackServiceDetailLoadingStatistics } from "../../utils/analytics"; +import { withRefreshApiCall } from "../../features/fastLogin/saga/utils"; +import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selectors"; /** * A generator to load the service details from the Backend @@ -38,14 +47,25 @@ export function* loadServiceDetailRequestHandler( throw Error("Unable to decode ServiceId to PathTraversalSafePathParam"); } - const response = yield* call(getService, { - service_id: serviceIdEither.right - }); + const response = (yield* call( + withRefreshApiCall, + getService({ + service_id: serviceIdEither.right + }), + action + )) as unknown as SagaCallReturnType; if (E.isLeft(response)) { throw Error(readableReport(response.left)); } + if (response.right.status === 401) { + const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); + if (isFastLoginEnabled) { + return; + } + } + if (response.right.status === 200) { yield* put(loadServiceDetail.success(response.right.value)); diff --git a/ts/sagas/startup/loadVisibleServicesHandler.ts b/ts/sagas/startup/loadVisibleServicesHandler.ts index ccd264b77cf..8826c0a0527 100644 --- a/ts/sagas/startup/loadVisibleServicesHandler.ts +++ b/ts/sagas/startup/loadVisibleServicesHandler.ts @@ -1,6 +1,6 @@ import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import * as E from "fp-ts/lib/Either"; -import { call, put } from "typed-redux-saga/macro"; +import { call, put, select } from "typed-redux-saga/macro"; import { BackendClient } from "../../api/backend"; import { loadVisibleServices } from "../../store/actions/services"; import { ReduxSagaEffect, SagaCallReturnType } from "../../types/utils"; @@ -8,6 +8,7 @@ import { convertUnknownToError } from "../../utils/errors"; import { refreshStoredServices } from "../services/refreshStoredServices"; import { removeUnusedStoredServices } from "../services/removeUnusedStoredServices"; import { withRefreshApiCall } from "../../features/fastLogin/saga/utils"; +import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selectors"; /** * A generator to load the service details from the Backend @@ -24,13 +25,19 @@ export function* loadVisibleServicesRequestHandler( SagaCallReturnType > { try { - const response = (yield* call( - withRefreshApiCall, - getVisibleServices({}) - )) as unknown as SagaCallReturnType; + const response = (yield* call(withRefreshApiCall, getVisibleServices({}), { + skipThrowingError: true + })) as unknown as SagaCallReturnType; + if (E.isLeft(response)) { throw Error(readableReport(response.left)); } + if (response.right.status === 401) { + const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); + if (isFastLoginEnabled) { + return; + } + } if (response.right.status === 200) { const { items: visibleServices } = response.right.value; yield* put(loadVisibleServices.success(visibleServices)); diff --git a/ts/sagas/user/userMetadata.ts b/ts/sagas/user/userMetadata.ts index b0cf4cf1452..24e3794d310 100644 --- a/ts/sagas/user/userMetadata.ts +++ b/ts/sagas/user/userMetadata.ts @@ -21,6 +21,8 @@ import { } from "../../store/reducers/userMetadata"; import { ReduxSagaEffect, SagaCallReturnType } from "../../types/utils"; import { convertUnknownToError } from "../../utils/errors"; +import { withRefreshApiCall } from "../../features/fastLogin/saga/utils"; +import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selectors"; /** * A saga to fetch user metadata from the Backend @@ -33,14 +35,24 @@ export function* fetchUserMetadata( SagaCallReturnType > { try { - const response = yield* call(getUserMetadata, {}); + const response = (yield* call(withRefreshApiCall, getUserMetadata({}), { + skipThrowingError: true + })) as unknown as SagaCallReturnType; // Can't decode response if (E.isLeft(response)) { throw Error(readableReport(response.left)); } - if (response.right.status !== 200) { + if (response.right.status === 401) { + const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); + if (isFastLoginEnabled) { + // Return an empty object cause we have a token refresh + // The flow that called this API, must recall it once the token is refreshed + return E.right(emptyUserMetadata); + } + } + if (response.right.status === 204) { // Return an empty object cause profile has no metadata yet (204 === No Content) return E.right(emptyUserMetadata); diff --git a/ts/sagas/wallet.ts b/ts/sagas/wallet.ts index 8f69b67b26e..82eb6f06a7d 100644 --- a/ts/sagas/wallet.ts +++ b/ts/sagas/wallet.ts @@ -87,7 +87,7 @@ import { pspForPaymentV2, pspForPaymentV2WithCallbacks, runDeleteActivePaymentSaga, - runStartOrResumePaymentActivationSaga + walletPaymentHandlersInitialized } from "../store/actions/wallet/payment"; import { fetchPsp, @@ -341,100 +341,6 @@ function* startOrResumeAddCreditCardSaga( resetLookUpId(); } -/** - * This saga will run in sequence the requests needed to activate a payment: - * - * 1) attiva -> nodo - * 2) polling for a payment id <- nodo - * 3) check -> payment manager - * - * Each step has a corresponding state in the wallet.payment state that gets - * updated with the "pot" state (none -> loading -> some|error). - * - * Each time the saga is run, it will resume from the next step that needs to - * be executed (either because it never executed or because it previously - * returned an error). - * - * Not that the pagoPA activation flow is not really resumable in case a step - * returns an error (i.e. the steps are not idempotent). - * - * TODO: the resume logic may be made more intelligent by analyzing the error - * of each step and proceeed to the next step under certain conditions - * (e.g. when resuming a previous payment flow from scratch, some steps - * may fail because they are not idempotent, but we could just proceed - * to the next step). - */ -// eslint-disable-next-line -function* startOrResumePaymentActivationSaga( - action: ActionType -) { - while (true) { - // before each step we select the updated payment state to know what has - // been already done. - const paymentState: GlobalState["wallet"]["payment"] = yield* select( - _ => _.wallet.payment - ); - - // first step: Attiva - if (pot.isNone(paymentState.attiva)) { - // this step needs to be executed - yield* put( - paymentAttiva.request({ - rptId: action.payload.rptId, - verifica: action.payload.verifica - }) - ); - const responseAction = yield* take< - ActionType - >([paymentAttiva.success, paymentAttiva.failure]); - if (isActionOf(paymentAttiva.failure, responseAction)) { - // this step failed, exit the flow - return; - } - // all is ok, continue to the next step - continue; - } - - // second step: poll for payment ID - if (pot.isNone(paymentState.paymentId)) { - // this step needs to be executed - yield* put(paymentIdPolling.request(action.payload.verifica)); - const responseAction = yield* take< - ActionType< - typeof paymentIdPolling.success | typeof paymentIdPolling.failure - > - >([paymentIdPolling.success, paymentIdPolling.failure]); - if (isActionOf(paymentIdPolling.failure, responseAction)) { - // this step failed, exit the flow - return; - } - // all is ok, continue to the next step - continue; - } - - // third step: "check" the payment - if (pot.isNone(paymentState.check)) { - // this step needs to be executed - yield* put(paymentCheck.request(paymentState.paymentId.value)); - const responseAction = yield* take< - ActionType - >([paymentCheck.success, paymentCheck.failure]); - if (isActionOf(paymentCheck.failure, responseAction)) { - // this step failed, exit the flow - return; - } - // all is ok, continue to the next step - continue; - } - - // finally, we signal the success of the activation flow - action.payload.onSuccess(paymentState.paymentId.value); - - // since this is the last step, we exit the flow - break; - } -} - /** * This saga attempts to delete the active payment, if there's one. * @@ -560,11 +466,6 @@ export function* watchWalletSaga( pmSessionManager ); - yield* takeLatest( - getType(runStartOrResumePaymentActivationSaga), - startOrResumePaymentActivationSaga - ); - yield* takeLatest( getType(runDeleteActivePaymentSaga), deleteActivePaymentSaga @@ -714,6 +615,8 @@ export function* watchWalletSaga( pmSessionManager ); + yield* put(walletPaymentHandlersInitialized()); + /** * whenever the profile is loaded (from a load request or from un update) * check if the email is validated. If it not the session manager has to be disabled @@ -901,8 +804,9 @@ export function* watchBackToEntrypointPaymentSaga(): Iterator { if (entrypointRoute !== undefined) { yield* call( NavigationService.dispatchNavigationAction, - CommonActions.navigate(entrypointRoute.name, { - key: entrypointRoute.key + CommonActions.navigate({ + name: entrypointRoute.name, + merge: true }) ); yield* put(paymentInitializeState()); diff --git a/ts/sagas/wallet/pagopaApis.ts b/ts/sagas/wallet/pagopaApis.ts index dacafac92bb..f273f43833f 100644 --- a/ts/sagas/wallet/pagopaApis.ts +++ b/ts/sagas/wallet/pagopaApis.ts @@ -5,6 +5,7 @@ import * as O from "fp-ts/lib/Option"; import { call, put, select, take } from "typed-redux-saga/macro"; import { ActionType, isActionOf } from "typesafe-actions"; import { Action } from "redux"; +import { IOToast } from "@pagopa/io-app-design-system"; import { BackendClient } from "../../api/backend"; import { PaymentManagerClient } from "../../api/pagopa"; import { mixpanelTrack } from "../../mixpanel"; @@ -62,7 +63,6 @@ import { import { readablePrivacyReport } from "../../utils/reporters"; import { SessionManager } from "../../utils/SessionManager"; import { convertWalletV2toWalletV1 } from "../../utils/walletv2"; -import { IOToast } from "../../components/Toast"; import I18n from "../../i18n"; import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse"; import { Detail_v2Enum } from "../../../definitions/backend/PaymentProblemJson"; @@ -733,6 +733,9 @@ export function* commonPaymentVerificationProcedure
( const details = response.right.value.detail_v2; const failureAction = failureActionProvider(details); yield* put(failureAction); + } else if (response.right.status === 401) { + // This status code does not represent an error to show to the user + // The authentication will be handled by the Fast Login token refresh procedure } else { throw Error(`response status ${response.right.status}`); } @@ -785,6 +788,9 @@ export function* paymentAttivaRequestHandler( ) { // Attiva failed throw Error(response.right.value.detail_v2); + } else if (response.right.status === 401) { + // This status code does not represent an error to show to the user + // The authentication will be handled by the Fast Login token refresh procedure } else { throw Error(`response status ${response.right.status}`); } @@ -814,13 +820,18 @@ export function* paymentIdPollingRequestHandler( yield* select(isPagoPATestEnabledSelector); const getPaymentId = getPaymentIdApi.e2; - const response: SagaCallReturnType = yield* call( - getPaymentId, - { - codiceContestoPagamento: action.payload.codiceContestoPagamento, - test: isPagoPATestEnabled - } - ); + + const request = getPaymentId({ + codiceContestoPagamento: action.payload.codiceContestoPagamento, + test: isPagoPATestEnabled + }); + + const response: SagaCallReturnType = (yield* call( + withRefreshApiCall, + request, + action + )) as unknown as SagaCallReturnType; + if (E.isRight(response)) { // Attiva succeeded if (response.right.status === 200) { diff --git a/ts/screens/analytics/emailAnalytics.ts b/ts/screens/analytics/emailAnalytics.ts index 07a21cb668e..3ee1d20e770 100644 --- a/ts/screens/analytics/emailAnalytics.ts +++ b/ts/screens/analytics/emailAnalytics.ts @@ -42,3 +42,38 @@ export function trackEmailValidationSuccessConfirmed(flow: FlowType) { buildEventProperties("UX", "action", undefined, flow) ); } + +export function trackEmailNotAlreadyConfirmed(flow: FlowType) { + void mixpanelTrack( + "EMAIL_VALIDATION_STOP", + buildEventProperties("UX", "screen_view", undefined, flow) + ); +} + +export function trackEmailAlreadyTaken(flow: FlowType) { + void mixpanelTrack( + "EMAIL_DUPLICATE_STOP", + buildEventProperties("UX", "screen_view", undefined, flow) + ); +} + +export function trackSendValidationEmail(flow: FlowType) { + void mixpanelTrack( + "EMAIL_VALIDATION_SEND", + buildEventProperties("UX", "action", undefined, flow) + ); +} + +export function trackResendValidationEmail(flow: FlowType) { + void mixpanelTrack( + "EMAIL_VALIDATION_RESEND", + buildEventProperties("UX", "action", undefined, flow) + ); +} + +export function trackEmailDuplicateEditingConfirm(flow: FlowType) { + void mixpanelTrack( + "EMAIL_DUPLICATE_EDITING_CONFIRM", + buildEventProperties("UX", "action", undefined, flow) + ); +} diff --git a/ts/screens/authentication/IdpLoginScreen.tsx b/ts/screens/authentication/IdpLoginScreen.tsx index 8e6024fe079..47a6fe1d9ad 100644 --- a/ts/screens/authentication/IdpLoginScreen.tsx +++ b/ts/screens/authentication/IdpLoginScreen.tsx @@ -16,7 +16,7 @@ import { IdpSuccessfulAuthentication } from "../../components/IdpSuccessfulAuthe import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; import IdpCustomContextualHelpContent from "../../components/screens/IdpCustomContextualHelpContent"; -import Markdown from "../../components/ui/Markdown"; +import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; import { RefreshIndicator } from "../../components/ui/RefreshIndicator"; import { useLollipopLoginSource } from "../../features/lollipop/hooks/useLollipopLoginSource"; import I18n from "../../i18n"; @@ -249,9 +249,9 @@ const IdpLoginScreen = (props: Props) => { return { title: I18n.t("authentication.idp_login.contextualHelpTitle"), body: () => ( - + {I18n.t("authentication.idp_login.contextualHelpContent")} - + ) }; } diff --git a/ts/screens/authentication/LandingScreen.tsx b/ts/screens/authentication/LandingScreen.tsx index 3053331215e..90adc7bbd77 100644 --- a/ts/screens/authentication/LandingScreen.tsx +++ b/ts/screens/authentication/LandingScreen.tsx @@ -2,76 +2,64 @@ * A screen where the user can choose to login with SPID or get more informations. * It includes a carousel with highlights on the app functionalities */ +import { + ButtonLink, + ButtonSolid, + ContentWrapper, + VSpacer +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { pipe } from "fp-ts/lib/function"; +import { useFocusEffect } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import JailMonkey from "jail-monkey"; -import { Content, Text as NBButtonText } from "native-base"; import * as React from "react"; -import { View, Alert, StyleSheet } from "react-native"; +import { Alert, View } from "react-native"; import DeviceInfo from "react-native-device-info"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useDispatch, useStore } from "react-redux"; -import { IOColors, Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; -import sessionExpiredImg from "../../../img/landing/session_expired.png"; -import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; -import CieNotSupported from "../../components/cie/CieNotSupported"; -import ContextualInfo from "../../components/ContextualInfo"; -import { Link } from "../../components/core/typography/Link"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { DevScreenButton } from "../../components/DevScreenButton"; -import { HorizontalScroll } from "../../components/HorizontalScroll"; -import { renderInfoRasterImage } from "../../components/infoScreen/imageRendering"; -import { InfoScreenComponent } from "../../components/infoScreen/InfoScreenComponent"; +import { SpidIdp } from "../../../definitions/content/SpidIdp"; import { LandingCardComponent } from "../../components/LandingCardComponent"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; -import BaseScreenComponent, { - ContextualHelpPropsMarkdown -} from "../../components/screens/BaseScreenComponent"; import SectionStatusComponent from "../../components/SectionStatus"; +import { IOStyles } from "../../components/core/variables/IOStyles"; +import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; +import { privacyUrl } from "../../config"; +import { isCieLoginUatEnabledSelector } from "../../features/cieLogin/store/selectors"; +import { cieFlowForDevServerEnabled } from "../../features/cieLogin/utils"; +import { + fastLoginOptInFFEnabled, + isFastLoginEnabledSelector +} from "../../features/fastLogin/store/selectors"; +import { useHeaderSecondLevel } from "../../hooks/useHeaderSecondLevel"; import I18n from "../../i18n"; import { mixpanelTrack } from "../../mixpanel"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import ROUTES from "../../navigation/routes"; import { idpSelected, resetAuthenticationState } from "../../store/actions/authentication"; -import { - hasApiLevelSupportSelector, - hasNFCFeatureSelector, - isCieSupportedSelector -} from "../../store/reducers/cie"; +import { useIOSelector } from "../../store/hooks"; +import { isSessionExpiredSelector } from "../../store/reducers/authentication"; +import { isCieSupportedSelector } from "../../store/reducers/cie"; import { continueWithRootOrJailbreakSelector } from "../../store/reducers/persistedPreferences"; -import variables from "../../theme/variables"; import { ComponentProps } from "../../types/react"; -import { isDevEnv } from "../../utils/environment"; -import RootedDeviceModal from "../modal/RootedDeviceModal"; -import { SpidIdp } from "../../../definitions/content/SpidIdp"; -import { openWebUrl } from "../../utils/url"; -import { cieSpidMoreInfoUrl } from "../../config"; -import { - fastLoginOptInFFEnabled, - isFastLoginEnabledSelector -} from "../../features/fastLogin/store/selectors"; -import { isCieLoginUatEnabledSelector } from "../../features/cieLogin/store/selectors"; -import { cieFlowForDevServerEnabled } from "../../features/cieLogin/utils"; +import { setAccessibilityFocus } from "../../utils/accessibility"; import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; -import { useIOSelector } from "../../store/hooks"; -import { isSessionExpiredSelector } from "../../store/reducers/authentication"; -import { LightModalContext } from "../../components/ui/LightModal"; -import { continueWithRootOrJailbreak } from "../../store/actions/persistedPreferences"; +import { openWebUrl } from "../../utils/url"; import { trackCieLoginSelected, trackMethodInfo, trackSpidLoginSelected } from "./analytics"; +import { Carousel } from "./carousel/Carousel"; -const getCards = ( - isCIEAvailable: boolean -): ReadonlyArray> => [ +const carouselCards: ReadonlyArray< + ComponentProps +> = [ { - id: 5, - image: require("../../../img/landing/05.png"), + id: 0, + pictogramName: "hello", title: I18n.t("authentication.landing.card5-title"), content: I18n.t("authentication.landing.card5-content"), accessibilityLabel: `${I18n.t( @@ -85,7 +73,7 @@ const getCards = ( }, { id: 1, - image: require("../../../img/landing/01.png"), + pictogramName: "star", title: I18n.t("authentication.landing.card1-title"), content: I18n.t("authentication.landing.card1-content"), accessibilityLabel: `${I18n.t( @@ -94,7 +82,7 @@ const getCards = ( }, { id: 2, - image: require("../../../img/landing/02.png"), + pictogramName: "cardFavourite", title: I18n.t("authentication.landing.card2-title"), content: I18n.t("authentication.landing.card2-content"), accessibilityLabel: `${I18n.t( @@ -103,27 +91,12 @@ const getCards = ( }, { id: 3, - image: require("../../../img/landing/03.png"), + pictogramName: "doc", title: I18n.t("authentication.landing.card3-title"), content: I18n.t("authentication.landing.card3-content"), accessibilityLabel: `${I18n.t( "authentication.landing.card3-title" )}. ${I18n.t("authentication.landing.card3-content")}` - }, - { - id: 4, - image: isCIEAvailable - ? require("../../../img/cie/CIE-onboarding-illustration.png") - : require("../../../img/landing/04.png"), - title: isCIEAvailable - ? I18n.t("authentication.landing.loginSpidCie") - : I18n.t("authentication.landing.card4-title"), - content: isCIEAvailable - ? I18n.t("authentication.landing.loginSpidCieContent") - : I18n.t("authentication.landing.card4-content"), - accessibilityLabel: `${I18n.t( - "authentication.landing.card4-title" - )}. ${I18n.t("authentication.landing.card4-content")}` } ]; @@ -132,30 +105,6 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { body: "authentication.landing.contextualHelpContent" }; -const styles = StyleSheet.create({ - flex: { - flex: 1 - }, - uatCie: { - backgroundColor: IOColors.red - }, - noCie: { - // don't use opacity since the button still have the active color when it is pressed - // TODO: Remove this half-disabled state. - // See also discussion on Slack: https://pagopaspa.slack.com/archives/C012L0U4NQL/p1657171504522639 - backgroundColor: IOColors.noCieButton - }, - fullOpacity: { - backgroundColor: variables.brandPrimary - }, - link: { - textAlign: "center", - paddingBottom: 5, - paddingTop: 4.5, - lineHeight: 30 - } -}); - export const IdpCIE: SpidIdp = { id: "cie", name: "CIE", @@ -163,7 +112,13 @@ export const IdpCIE: SpidIdp = { profileUrl: "" }; +const SPACE_BETWEEN_BUTTONS = 8; +const SPACE_AROUND_BUTTON_LINK = 16; + export const LandingScreen = () => { + const accessibilityFirstFocuseViewRef = React.useRef(null); + const insets = useSafeAreaInsets(); + const [isRootedOrJailbroken, setIsRootedOrJailbroken] = React.useState< O.Option >(O.none); @@ -175,9 +130,14 @@ export const LandingScreen = () => { const store = useStore(); const dispatch = useDispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const isSessionExpired = useIOSelector(isSessionExpiredSelector); + // Since the page is rendered more than once + // and if the session is expired + // we dispatch the resetAuthenticationState action, + // we need to keep track of the session expiration. + const isSessionExpiredRef = React.useRef(false); const isContinueWithRootOrJailbreak = useIOSelector( continueWithRootOrJailbreakSelector @@ -187,10 +147,6 @@ export const LandingScreen = () => { const isFastLoginOptInFFEnabled = useIOSelector(fastLoginOptInFFEnabled); const isCIEAuthenticationSupported = useIOSelector(isCieSupportedSelector); - const hasApiLevelSupport = useIOSelector(hasApiLevelSupportSelector); - const hasCieApiLevelSupport = pot.getOrElse(hasApiLevelSupport, false); - const hasNFCFeature = useIOSelector(hasNFCFeatureSelector); - const hasCieNFCFeature = pot.getOrElse(hasNFCFeature, false); const isCieSupported = React.useCallback( () => @@ -200,17 +156,29 @@ export const LandingScreen = () => { ); const isCieUatEnabled = useIOSelector(isCieLoginUatEnabledSelector); + useFocusEffect(() => setAccessibilityFocus(accessibilityFirstFocuseViewRef)); + useOnFirstRender(async () => { - const isRootedOrJailbroken = await JailMonkey.isJailBroken(); - setIsRootedOrJailbroken(O.some(isRootedOrJailbroken)); + const isRootedOrJailbrokenFromJailMonkey = await JailMonkey.isJailBroken(); + setIsRootedOrJailbroken(O.some(isRootedOrJailbrokenFromJailMonkey)); if (isSessionExpired) { + // eslint-disable-next-line functional/immutable-data + isSessionExpiredRef.current = isSessionExpired; dispatch(resetAuthenticationState()); } }); - const { hideModal, showAnimatedModal } = React.useContext(LightModalContext); + // We reset the session expiration flag + // when the component is unmounted + React.useEffect( + () => () => { + // eslint-disable-next-line functional/immutable-data + isSessionExpiredRef.current = false; + }, + [] + ); - const displayTabletAlert = () => { + const displayTabletAlert = React.useCallback(() => { if (!hasTabletCompatibilityAlertAlreadyShown) { setHasTabletCompatibilityAlertAlreadyShown(true); Alert.alert( @@ -225,15 +193,7 @@ export const LandingScreen = () => { { cancelable: true } ); } - }; - - const navigateToMarkdown = React.useCallback( - () => - navigation.navigate(ROUTES.AUTHENTICATION, { - screen: ROUTES.MARKDOWN - }), - [navigation] - ); + }, [hasTabletCompatibilityAlertAlreadyShown]); const navigateToIdpSelection = React.useCallback(() => { trackSpidLoginSelected(); @@ -250,21 +210,6 @@ export const LandingScreen = () => { }, [isFastLoginOptInFFEnabled, navigation]); const navigateToCiePinScreen = React.useCallback(() => { - const openUnsupportedCIEModal = () => { - showAnimatedModal( - ( - - )} - /> - ); - }; - if (isCieSupported()) { void trackCieLoginSelected(store.getState()); dispatch(idpSelected(IdpCIE)); @@ -279,24 +224,16 @@ export const LandingScreen = () => { }); } } else { - openUnsupportedCIEModal(); + navigation.navigate(ROUTES.AUTHENTICATION, { + screen: ROUTES.CIE_NOT_SUPPORTED + }); } - }, [ - dispatch, - hasCieApiLevelSupport, - hasCieNFCFeature, - hideModal, - isCieSupported, - isFastLoginOptInFFEnabled, - navigation, - showAnimatedModal, - store - ]); + }, [dispatch, isCieSupported, isFastLoginOptInFFEnabled, navigation, store]); - const navigateToSpidCieInformationRequest = () => { + const navigateToPrivacyUrl = React.useCallback(() => { trackMethodInfo(); - openWebUrl(cieSpidMoreInfoUrl); - }; + openWebUrl(privacyUrl); + }, []); const navigateToCieUatSelectionScreen = React.useCallback(() => { if (isCieSupported()) { @@ -306,167 +243,139 @@ export const LandingScreen = () => { } }, [isCieSupported, navigation]); - const renderCardComponents = () => { - const cardProps = getCards(isCieSupported()); - return cardProps.map(p => ( - - )); - }; + const LandingScreen = () => { + useHeaderSecondLevel({ + title: "", + supportRequest: true, + canGoBack: false, + contextualHelpMarkdown + }); - const handleContinueWithRootOrJailbreak = (continueWith: boolean) => { - dispatch(continueWithRootOrJailbreak(continueWith)); - }; + const sessionExpiredCardContent = I18n.t( + "authentication.landing.session_expired.body", + { + days: isFastLoginEnabled ? "365" : "30" + } + ); - // eslint-disable-next-line sonarjs/cognitive-complexity - const renderLandingScreen = () => { - const firstButtonStyle = isCieUatEnabled - ? styles.uatCie - : styles.fullOpacity; - const secondButtonStyle = isCieSupported() - ? styles.fullOpacity - : styles.noCie; return ( - - {isDevEnv && } - - {isSessionExpired ? ( - + {isSessionExpiredRef.current ? ( + ) : ( - - - + )} - - - isCieSupported() ? navigateToCieUatSelectionScreen() : "" + + + + - - - - {isCieSupported() - ? I18n.t("authentication.landing.loginCie") - : I18n.t("authentication.landing.loginSpid")} - - - - - + + + - - - {isCieSupported() - ? I18n.t("authentication.landing.loginSpid") - : I18n.t("authentication.landing.loginCie")} - - - - - {isCieSupported() - ? I18n.t("authentication.landing.nospid-nocie") - : I18n.t("authentication.landing.nospid")} - - - + + + {insets.bottom !== 0 && } + + ); }; // Screen displayed during the async loading of the JailMonkey.isJailBroken() - const renderLoadingScreen = () => ( + const LoadingScreen = () => ( ); - const chooseScreenToRender = (isRootedOrJailbroken: boolean) => { + React.useEffect(() => { // if the device is compromised and the user didn't allow to continue // show a blocking modal - if (isRootedOrJailbroken && !isContinueWithRootOrJailbreak) { + if ( + O.isSome(isRootedOrJailbroken) && + isRootedOrJailbroken.value && + !isContinueWithRootOrJailbreak + ) { void mixpanelTrack("SHOW_ROOTED_OR_JAILBROKEN_MODAL"); - return ( - handleContinueWithRootOrJailbreak(true)} - onCancel={() => handleContinueWithRootOrJailbreak(false)} - /> - ); + navigation.navigate(ROUTES.AUTHENTICATION, { + screen: ROUTES.AUTHENTICATION_ROOTED_DEVICE + }); } - // In case of Tablet, display an alert to inform the user + }, [isContinueWithRootOrJailbreak, isRootedOrJailbroken, navigation]); + + // If the async loading of the isRootedOrJailbroken is not ready, display a loading + if (O.isNone(isRootedOrJailbroken)) { + return ; + } else { if (DeviceInfo.isTablet()) { displayTabletAlert(); } - // standard rendering of the landing screen - return renderLandingScreen(); - }; - - // If the async loading of the isRootedOrJailbroken is not ready, display a loading - return pipe( - isRootedOrJailbroken, - O.fold( - () => renderLoadingScreen(), - // when the value isRootedOrJailbroken is ready, display the right screen based on a set of rule - rootedOrJailbroken => chooseScreenToRender(rootedOrJailbroken) - ) - ); + return ; + } }; diff --git a/ts/screens/authentication/NewOptInScreen.tsx b/ts/screens/authentication/NewOptInScreen.tsx index 5c088079a8f..456d5649804 100644 --- a/ts/screens/authentication/NewOptInScreen.tsx +++ b/ts/screens/authentication/NewOptInScreen.tsx @@ -10,14 +10,11 @@ import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; import { useStore } from "react-redux"; -import BaseScreenComponent, { - ContextualHelpPropsMarkdown -} from "../../components/screens/BaseScreenComponent"; +import { Route, useFocusEffect, useRoute } from "@react-navigation/native"; +import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import ROUTES from "../../navigation/routes"; -import { AuthenticationParamsList } from "../../navigation/params/AuthenticationParamsList"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import I18n from "../../i18n"; import { setFastLoginOptIn } from "../../features/fastLogin/store/actions/optInActions"; import { useIODispatch } from "../../store/hooks"; @@ -29,6 +26,8 @@ import { trackLoginSessionOptInInfo } from "../../features/fastLogin/analytics/optinAnalytics"; import { useSecuritySuggestionsBottomSheet } from "../../hooks/useSecuritySuggestionBottomSheet"; +import { setAccessibilityFocus } from "../../utils/accessibility"; +import { useHeaderSecondLevel } from "../../hooks/useHeaderSecondLevel"; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "authentication.opt_in.contextualHelpTitle", @@ -41,28 +40,29 @@ export type ChosenIdentifier = { identifier: "SPID" | "CIE"; }; -type Props = IOStackNavigationRouteProps< - AuthenticationParamsList, - "AUTHENTICATION_OPT_IN" ->; +const NewOptInScreen = () => { + useHeaderSecondLevel({ + title: "", + supportRequest: true, + contextualHelpMarkdown + }); -const NewOptInScreen = (props: Props) => { + const accessibilityFirstFocuseViewRef = React.useRef(null); const dispatch = useIODispatch(); const { securitySuggestionBottomSheet, presentSecuritySuggestionBottomSheet } = useSecuritySuggestionsBottomSheet(); - - const navigation = useNavigation(); + const { identifier } = + useRoute>().params; + const navigation = useIONavigation(); const store = useStore(); useOnFirstRender(() => { trackLoginSessionOptIn(); }); - useOnFirstRender(() => { - trackLoginSessionOptIn(); - }); + useFocusEffect(() => setAccessibilityFocus(accessibilityFirstFocuseViewRef)); const navigateToIdpPage = (isLV: boolean) => { if (isLV) { @@ -72,7 +72,7 @@ const NewOptInScreen = (props: Props) => { } navigation.navigate(ROUTES.AUTHENTICATION, { screen: - props.route.params.identifier === "CIE" + identifier === "CIE" ? ROUTES.CIE_PIN_SCREEN : ROUTES.AUTHENTICATION_IDP_SELECTION }); @@ -80,74 +80,72 @@ const NewOptInScreen = (props: Props) => { }; return ( - navigateToIdpPage(true), + testID: "accept-button-test" + }} + secondaryActionProps={{ + label: I18n.t("authentication.opt_in.button_decline_lv"), + accessibilityLabel: I18n.t("authentication.opt_in.button_decline_lv"), + onPress: () => navigateToIdpPage(false), + testID: "decline-button-test" + }} > - navigateToIdpPage(true), - testID: "accept-button-test" - }} - secondaryActionProps={{ - label: I18n.t("authentication.opt_in.button_decline_lv"), - accessibilityLabel: I18n.t("authentication.opt_in.button_decline_lv"), - onPress: () => navigateToIdpPage(false), - testID: "decline-button-test" - }} - > - - {/* + + {/* if the device height is > 820 then the pictogram will be visible, otherwise it will not be visible */} - {Dimensions.get("screen").height > MIN_HEIGHT_TO_SHOW_FULL_RENDER && ( - - - - )} - - - + {Dimensions.get("screen").height > MIN_HEIGHT_TO_SHOW_FULL_RENDER && ( + + - + )} + + + + + +

{I18n.t("authentication.opt_in.title")}

- - - - - - { - trackLoginSessionOptInInfo(); - return presentSecuritySuggestionBottomSheet(); - }} - /> -
- {securitySuggestionBottomSheet} -
-
+
+ + + + + + { + trackLoginSessionOptInInfo(); + return presentSecuritySuggestionBottomSheet(); + }} + /> + + {securitySuggestionBottomSheet} + ); }; diff --git a/ts/screens/authentication/analytics/carouselAnalytics.ts b/ts/screens/authentication/analytics/carouselAnalytics.ts index 5bd5bae5536..dc5386d4b60 100644 --- a/ts/screens/authentication/analytics/carouselAnalytics.ts +++ b/ts/screens/authentication/analytics/carouselAnalytics.ts @@ -23,13 +23,6 @@ function trackCarouselPaymentScreen() { ); } -function trackCarouselAccessScreen() { - void mixpanelTrack( - "LOGIN_CAROUSEL_5", - buildEventProperties("UX", "screen_view") - ); -} - export function trackCarousel( index: number, cards: ReadonlyArray @@ -51,9 +44,5 @@ export function trackCarousel( trackCarouselPaymentScreen(); break; } - case 4: { - trackCarouselAccessScreen(); - break; - } } } diff --git a/ts/screens/authentication/analytics/index.ts b/ts/screens/authentication/analytics/index.ts index cdc60cb5a28..a490765049b 100644 --- a/ts/screens/authentication/analytics/index.ts +++ b/ts/screens/authentication/analytics/index.ts @@ -17,10 +17,7 @@ export async function trackCieLoginSelected(state: GlobalState) { property: "LOGIN_METHOD", value: IdpCIE.id }); - await mixpanelTrack( - "LOGIN_CIE_SELECTED", - buildEventProperties("UX", "action") - ); + mixpanelTrack("LOGIN_CIE_SELECTED", buildEventProperties("UX", "action")); } export function trackSpidLoginSelected() { diff --git a/ts/screens/authentication/analytics/spidAnalytics.ts b/ts/screens/authentication/analytics/spidAnalytics.ts index 159a28846c1..88d77876ec4 100644 --- a/ts/screens/authentication/analytics/spidAnalytics.ts +++ b/ts/screens/authentication/analytics/spidAnalytics.ts @@ -93,7 +93,7 @@ export async function trackLoginSpidIdpSelected( property: "LOGIN_METHOD", value: idp }); - await mixpanelTrack( + mixpanelTrack( "LOGIN_SPID_IDP_SELECTED", buildEventProperties("UX", "action", { idp diff --git a/ts/screens/authentication/carousel/Carousel.tsx b/ts/screens/authentication/carousel/Carousel.tsx new file mode 100644 index 00000000000..7cd7b23e2f5 --- /dev/null +++ b/ts/screens/authentication/carousel/Carousel.tsx @@ -0,0 +1,154 @@ +import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { + Animated, + ScrollView, + View, + StyleSheet, + GestureResponderEvent, + useWindowDimensions +} from "react-native"; +import { trackCarousel } from "../analytics/carouselAnalytics"; +import { LandingCardComponent } from "../../../components/LandingCardComponent"; +import { ComponentProps } from "../../../types/react"; +import { useInteractiveElementDefaultColorName } from "../../../utils/hooks/theme"; + +const styles = StyleSheet.create({ + normalDot: { + height: 8, + width: 8, + borderRadius: 4, + backgroundColor: IOColors.greyLight, + marginHorizontal: 4 + }, + indicatorContainer: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center" + } +}); + +const newDsGrey = IOColors["grey-200"]; + +type CarouselProps = { + carouselCards: ReadonlyArray>; + dotEasterEggCallback?: () => void; +}; + +type CarouselDotsProps = CarouselProps & { scrollX: Animated.Value }; + +const CarouselDots = (props: CarouselDotsProps) => { + const { carouselCards, dotEasterEggCallback, scrollX } = props; + const dotTouchCount = React.useRef(0); + + const blueColor = useInteractiveElementDefaultColorName(); + + const screenDimension = useWindowDimensions(); + const windowWidth = screenDimension.width; + + return ( + { + // eslint-disable-next-line functional/immutable-data + dotTouchCount.current++; + if (dotTouchCount.current === 3) { + // eslint-disable-next-line functional/immutable-data + dotTouchCount.current = 0; + dotEasterEggCallback?.(); + } + }} + > + {carouselCards.map((_, imageIndex) => { + const width = scrollX.interpolate({ + inputRange: [ + windowWidth * (imageIndex - 1), + windowWidth * imageIndex, + windowWidth * (imageIndex + 1) + ], + outputRange: [8, 16, 8], + extrapolate: "clamp" + }); + const backgroundColor = scrollX.interpolate({ + inputRange: [ + windowWidth * (imageIndex - 1), + windowWidth * imageIndex, + windowWidth * (imageIndex + 1) + ], + outputRange: [newDsGrey, blueColor, newDsGrey], + extrapolate: "clamp" + }); + return ( + + ); + })} + + ); +}; + +export const Carousel = React.forwardRef((props, ref) => { + const { carouselCards, dotEasterEggCallback } = props; + const screenDimension = useWindowDimensions(); + const windowWidth = screenDimension.width; + const scrollX = React.useRef(new Animated.Value(0)).current; + + const renderCardComponents = React.useCallback( + () => + carouselCards.map(p => ( + + )), + [carouselCards, ref] + ); + + const cardComponents = renderCardComponents(); + + return ( + <> + { + const contentOffsetX = event.nativeEvent.contentOffset.x; + const currentPageIndex = Math.round(contentOffsetX / windowWidth); + if ( + currentPageIndex >= 0 && + cardComponents.length > currentPageIndex + ) { + trackCarousel(currentPageIndex, cardComponents); + } + }} + onScroll={Animated.event( + [ + { + nativeEvent: { + contentOffset: { + x: scrollX + } + } + } + ], + { useNativeDriver: false } + )} + scrollEventThrottle={1} + > + {cardComponents} + + + + + ); +}); diff --git a/ts/screens/authentication/cie/CieAuthorizeDataUsageScreen.tsx b/ts/screens/authentication/cie/CieAuthorizeDataUsageScreen.tsx index 1235014a393..1fda354675e 100644 --- a/ts/screens/authentication/cie/CieAuthorizeDataUsageScreen.tsx +++ b/ts/screens/authentication/cie/CieAuthorizeDataUsageScreen.tsx @@ -8,7 +8,7 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import { H1 } from "../../../components/core/typography/H1"; import TopScreenComponent from "../../../components/screens/TopScreenComponent"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../i18n"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import { AuthenticationParamsList } from "../../../navigation/params/AuthenticationParamsList"; @@ -63,9 +63,9 @@ class CieAuthorizeDataUsageScreen extends React.PureComponent {

{I18n.t("authentication.cie.noDataTitle")}

- + {I18n.t("authentication.cie.authToSendData")} - +
{this.state.isLoadingCompleted && ( diff --git a/ts/screens/authentication/cie/CieCardReaderScreen.tsx b/ts/screens/authentication/cie/CieCardReaderScreen.tsx index 9f9995e9a5d..ce82cc19d0c 100644 --- a/ts/screens/authentication/cie/CieCardReaderScreen.tsx +++ b/ts/screens/authentication/cie/CieCardReaderScreen.tsx @@ -444,7 +444,8 @@ class CieCardReaderScreen extends React.PureComponent { ), wrongPin2AttemptLeft: I18n.t( "authentication.cie.card.iosAlert.wrongPin2AttemptLeft" - ) + ), + genericError: I18n.t("authentication.cie.card.iosAlert.genericError") }) .then(async () => { await cieManager.startListeningNFC(); diff --git a/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx b/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx index 42ae9307e26..9dc9877ef5e 100644 --- a/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx +++ b/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx @@ -11,12 +11,11 @@ import { } from "react-native-webview/lib/WebViewTypes"; import { connect } from "react-redux"; import { VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay"; import GenericErrorComponent from "../../../components/screens/GenericErrorComponent"; import TopScreenComponent from "../../../components/screens/TopScreenComponent"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { AuthenticationParamsList } from "../../../navigation/params/AuthenticationParamsList"; import { loginFailure, loginSuccess @@ -39,24 +38,13 @@ export type CieConsentDataUsageScreenNavigationParams = { cieConsentUri: string; }; -type OwnProps = { - isLoading: boolean; -}; - -type NavigationProps = IOStackNavigationRouteProps< - AuthenticationParamsList, - "CIE_CONSENT_DATA_USAGE" ->; - type State = { hasError: boolean; errorCode?: string; isLoginSuccess?: boolean; }; -type Props = OwnProps & - NavigationProps & - ReturnType & +type Props = ReturnType & ReturnType; const loaderComponent = ( @@ -65,9 +53,14 @@ const loaderComponent = ( ); -class CieConsentDataUsageScreen extends React.Component { +type CieConsentDataUsageScreenProps = Props & + CieConsentDataUsageScreenNavigationParams; +class CieConsentDataUsageScreen extends React.Component< + CieConsentDataUsageScreenProps, + State +> { private subscription: NativeEventSubscription | undefined; - constructor(props: Props) { + constructor(props: CieConsentDataUsageScreenProps) { super(props); trackLoginCieConsentDataUsageScreen(); this.state = { @@ -113,7 +106,7 @@ class CieConsentDataUsageScreen extends React.Component { } get cieAuthorizationUri(): string { - return this.props.route.params.cieConsentUri; + return this.props.cieConsentUri; } private handleWebViewError = () => { @@ -224,7 +217,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ loginFailure: (error: Error) => dispatch(loginFailure({ error, idp: "cie" })) }); +const CieConsentDataUsageScreenFC = (props: Props) => { + const { cieConsentUri } = + useRoute< + Route<"CIE_CONSENT_DATA_USAGE", CieConsentDataUsageScreenNavigationParams> + >().params; + return ; +}; + export default connect( mapStateToProps, mapDispatchToProps -)(CieConsentDataUsageScreen); +)(CieConsentDataUsageScreenFC); diff --git a/ts/screens/authentication/cie/CieExpiredOrInvalidScreen.tsx b/ts/screens/authentication/cie/CieExpiredOrInvalidScreen.tsx index 15d1ccd241b..06541a5bfd0 100644 --- a/ts/screens/authentication/cie/CieExpiredOrInvalidScreen.tsx +++ b/ts/screens/authentication/cie/CieExpiredOrInvalidScreen.tsx @@ -1,6 +1,5 @@ import { Content } from "native-base"; import * as React from "react"; -import { connect } from "react-redux"; import { VSpacer } from "@pagopa/io-app-design-system"; import { Body } from "../../../components/core/typography/Body"; import { Link } from "../../../components/core/typography/Link"; @@ -10,46 +9,38 @@ import FooterWithButtons from "../../../components/ui/FooterWithButtons"; import { openLink } from "../../../components/ui/Markdown/handlers/link"; import I18n from "../../../i18n"; import { resetToAuthenticationRoute } from "../../../store/actions/navigation"; -import { ReduxProps } from "../../../store/actions/types"; -type Props = ReduxProps; const bookingUrl = I18n.t("cie.booking_url"); const browseToLink = () => openLink(bookingUrl); -class CieExpiredOrInvalidScreen extends React.PureComponent { - constructor(props: Props) { - super(props); - } +const CieExpiredOrInvalidScreen = () => { + const handleGoBack = () => resetToAuthenticationRoute(); - private handleGoBack = () => resetToAuthenticationRoute(); + return ( + + + + {I18n.t("authentication.landing.expiredCardContent")} + + + {I18n.t("authentication.landing.expiredCardHelp")} + + + + + ); +}; - public render(): React.ReactNode { - return ( - - - - {I18n.t("authentication.landing.expiredCardContent")} - - - {I18n.t("authentication.landing.expiredCardHelp")} - - - - - ); - } -} - -export default connect()(CieExpiredOrInvalidScreen); +export default CieExpiredOrInvalidScreen; diff --git a/ts/screens/authentication/cie/CiePinLockedTemporarilyScreen.tsx b/ts/screens/authentication/cie/CiePinLockedTemporarilyScreen.tsx index 6cd30c4596d..9f1e87c4fa4 100644 --- a/ts/screens/authentication/cie/CiePinLockedTemporarilyScreen.tsx +++ b/ts/screens/authentication/cie/CiePinLockedTemporarilyScreen.tsx @@ -9,7 +9,7 @@ import { connect } from "react-redux"; import { ScreenContentHeader } from "../../../components/screens/ScreenContentHeader"; import TopScreenComponent from "../../../components/screens/TopScreenComponent"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../i18n"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import { AuthenticationParamsList } from "../../../navigation/params/AuthenticationParamsList"; @@ -45,7 +45,9 @@ class CiePinLockedTemporarilyScreen extends React.PureComponent { private getContextualHelp = () => ({ title: I18n.t("authentication.cie.pin.contextualHelpTitle"), body: () => ( - {I18n.t("authentication.cie.pin.contextualHelpBody")} + + {I18n.t("authentication.cie.pin.contextualHelpBody")} + ) }); @@ -84,13 +86,13 @@ class CiePinLockedTemporarilyScreen extends React.PureComponent { title={I18n.t("authentication.cie.pinTempLocked.title")} /> - { this.setState({ isLoadingCompleted: true }); }} > {I18n.t("authentication.cie.pinTempLocked.content")} - + {this.state.isLoadingCompleted && this.renderFooterButtons()} diff --git a/ts/screens/authentication/cie/CiePinScreen.tsx b/ts/screens/authentication/cie/CiePinScreen.tsx index c80d8ab2f8c..22cbc7b9be6 100644 --- a/ts/screens/authentication/cie/CiePinScreen.tsx +++ b/ts/screens/authentication/cie/CiePinScreen.tsx @@ -29,13 +29,13 @@ import { BottomTopAnimation, LightModalContext } from "../../../components/ui/LightModal"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../i18n"; import { IOStackNavigationProp } from "../../../navigation/params/AppParamsList"; import { AuthenticationParamsList } from "../../../navigation/params/AuthenticationParamsList"; import ROUTES from "../../../navigation/routes"; import { nfcIsEnabled } from "../../../store/actions/cie"; -import { Dispatch, ReduxProps } from "../../../store/actions/types"; +import { Dispatch } from "../../../store/actions/types"; import variables from "../../../theme/variables"; import { setAccessibilityFocus } from "../../../utils/accessibility"; import { useLegacyIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; @@ -60,7 +60,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(loginSuccess({ token, idp })) }); -type Props = ReduxProps & ReturnType; +type Props = ReturnType; const styles = StyleSheet.create({ container: { @@ -79,7 +79,9 @@ const CIE_PIN_LENGTH = 8; const getContextualHelp = () => ({ title: I18n.t("authentication.cie.pin.contextualHelpTitle"), body: () => ( - {I18n.t("authentication.cie.pin.contextualHelpBody")} + + {I18n.t("authentication.cie.pin.contextualHelpBody")} + ) }); const onOpenForgotPinPage = () => openWebUrl(pinPukHelpUrl); @@ -109,9 +111,9 @@ const CiePinScreen: React.FC = props => { const { present, bottomSheet } = useLegacyIOBottomSheetModal( - + {I18n.t("bottomSheets.ciePin.content")} - + { const [requestInfo, setRequestInfo] = useState({ requestState: "LOADING", @@ -196,9 +197,9 @@ export const AuthSessionPage = () => { () => ({ title: I18n.t("authentication.idp_login.contextualHelpTitle"), body: () => ( - + {I18n.t("authentication.idp_login.contextualHelpContent")} - + ) }), idpTextData => IdpCustomContextualHelpContent(idpTextData) @@ -257,12 +258,12 @@ export const AuthSessionPage = () => { (error?: LoginUtilsError) => { void mixpanelTrack("SPID_ERROR", { idp, - description: error?.userInfo.Error, + description: error?.userInfo?.error, errorType: ErrorType.LOADING_ERROR }); const backPressed: LoginUtilsErrorType = "NativeAuthSessionClosed"; - if (error?.userInfo.Error === backPressed) { + if (error?.userInfo?.error === backPressed) { onBack(); return; } diff --git a/ts/screens/development/MarkdownScreen.tsx b/ts/screens/development/MarkdownScreen.tsx deleted file mode 100644 index 0f27ec0b880..00000000000 --- a/ts/screens/development/MarkdownScreen.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Container, Content, Text as NBButtonText } from "native-base"; -import * as React from "react"; -import { View, TextInput } from "react-native"; -import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; - -import Markdown from "../../components/ui/Markdown"; -import I18n from "../../i18n"; - -const MARKDOWN_REFERENCE = I18n.t("global.markdown.reference"); - -const MARKDOWN_HEADING = ` -# I am a Header 1 - -## I am a Header 2 - -### I am a Header 3 - -#### I am a Header 4 - -##### I am a Header 5 - -###### I am a Header 6 -`; - -const MARKDOWN_PARAGRAPH = ` -A simple paragraph. - -Text can be emphasized with *asterisk* or _underscore_. - -If you need bold use **double asterisk**. -`; - -const MARKDOWN_LIST = ` -Unordered list: - -* React -* Vue -* Angular - -Ordered list: - -1. React -2. Vue -3. Angular -`; - -type Props = Record; - -type State = { - markdown: string; -}; - -const INITIAL_STATE: State = { - markdown: "" -}; - -class MarkdownScreen extends React.Component { - constructor(props: Props) { - super(props); - this.state = INITIAL_STATE; - } - - private setMarkdown(markdown: string) { - this.setState({ - markdown - }); - } - - public render() { - return ( - - - - this.setMarkdown(MARKDOWN_HEADING)} - > - Heading - - this.setMarkdown(MARKDOWN_PARAGRAPH)} - > - Paragraph - - this.setMarkdown(MARKDOWN_LIST)} - > - List - - this.setMarkdown(MARKDOWN_REFERENCE)} - > - Reference - - - - this.setState({ markdown: text })} - value={this.state.markdown} - multiline={true} - numberOfLines={10} - /> - - {this.state.markdown} - - - ); - } -} - -export default MarkdownScreen; diff --git a/ts/screens/ingress/CheckBox.tsx b/ts/screens/ingress/CheckBox.tsx deleted file mode 100644 index dc8bbf7ddfb..00000000000 --- a/ts/screens/ingress/CheckBox.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { StyleSheet, View } from "react-native"; -import { - IOColors, - IOIconSizeScaleCheckbox, - Icon, - hexToRgba -} from "@pagopa/io-app-design-system"; - -type Props = { - checked: boolean; -}; - -const checkBoxColor = hexToRgba(IOColors.white, 0.15); -const CHECKBOX_SIZE: number = 20; -const CHECKBOX_ICON_SIZE: IOIconSizeScaleCheckbox = 14; - -const styles = StyleSheet.create({ - base: { - width: CHECKBOX_SIZE, - height: CHECKBOX_SIZE, - alignItems: "center", - justifyContent: "center", - borderWidth: 2, - borderColor: checkBoxColor, - borderRadius: 4 - }, - checked: { - borderColor: `transparent`, - backgroundColor: checkBoxColor - } -}); - -export const IngressCheckBox = (props: Props) => ( - - {props.checked && ( - - )} - -); diff --git a/ts/screens/ingress/IngressScreen.tsx b/ts/screens/ingress/IngressScreen.tsx index 5f627637caa..5836f3e15ed 100644 --- a/ts/screens/ingress/IngressScreen.tsx +++ b/ts/screens/ingress/IngressScreen.tsx @@ -1,104 +1,68 @@ /** * An ingress screen to choose the real first screen the user must navigate to. */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { Container, List, ListItem, Spinner } from "native-base"; import * as React from "react"; -import { StatusBar, StyleSheet, View, SafeAreaView } from "react-native"; -import { connect } from "react-redux"; -import { HSpacer } from "@pagopa/io-app-design-system"; -import { Body } from "../../components/core/typography/Body"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import SectionStatusComponent from "../../components/SectionStatus"; -import I18n from "../../i18n"; -import { ReduxProps } from "../../store/actions/types"; +import { View, StyleSheet, AccessibilityInfo, Platform } from "react-native"; import { - sessionInfoSelector, - sessionTokenSelector -} from "../../store/reducers/authentication"; -import { profileSelector } from "../../store/reducers/profile"; -import { GlobalState } from "../../store/reducers/types"; -import variables from "../../theme/variables"; -import { IngressCheckBox } from "./CheckBox"; - -type Props = ReduxProps & ReturnType; + ContentWrapper, + H3, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; +import { SafeAreaView } from "react-native-safe-area-context"; +import I18n from "../../i18n"; +import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; +import { trackIngressScreen } from "../profile/analytics"; +import { LoadingIndicator } from "../../components/ui/LoadingIndicator"; const styles = StyleSheet.create({ container: { - flex: 1, - paddingTop: variables.contentPaddingLarge, - backgroundColor: variables.brandPrimary + ...IOStyles.bgWhite, + ...IOStyles.centerJustified, + ...IOStyles.flex + }, + contentTitle: { + textAlign: "center" + }, + content: { + alignItems: "center" } }); -class IngressScreen extends React.PureComponent { - public render() { - const items = [ - { - enabled: this.props.hasSessionToken, - label: I18n.t("startup.authentication") - }, - { - enabled: this.props.hasSessionInfo, - label: I18n.t("startup.sessionInfo") - }, - { enabled: this.props.hasProfile, label: I18n.t("startup.profileInfo") }, - { - enabled: this.props.isProfileEnabled, - label: I18n.t("startup.profileEnabled") - } - ]; - return ( - - - - - {I18n.t("startup.title")} - - - - - {items.map((item, index) => ( - - - - - {item.label} - - - ))} - - - - - - - ); - } -} +const SPACE_BETWEEN_SPINNER_AND_TEXT = 24; -function mapStateToProps(state: GlobalState) { - const maybeSessionToken = sessionTokenSelector(state); - const maybeSessionInfo = sessionInfoSelector(state); - const potProfile = profileSelector(state); - return { - hasSessionToken: maybeSessionToken !== undefined, - hasSessionInfo: O.isSome(maybeSessionInfo), - hasProfile: potProfile !== null, - isProfileEnabled: - pot.isSome(potProfile) && - potProfile.value.has_profile && - potProfile.value.is_inbox_enabled - }; -} +export const IngressScreen = () => { + const contentTitle = I18n.t("startup.title"); + useOnFirstRender(() => { + trackIngressScreen(); + // Since the screen is shown for a very short time, + // we prefer to announce the content to the screen reader, + // instead of focusing the first element. + if (Platform.OS === "android") { + // We use it only on Android, because on iOS the screen reader + // stops reading the content when the ingress screen is unmounted + // and the focus is moved to another element. + AccessibilityInfo.announceForAccessibility(contentTitle); + } + }); -export default connect(mapStateToProps)(IngressScreen); + return ( + + + + + + + +

+ {contentTitle} +

+
+
+
+ ); +}; diff --git a/ts/screens/modal/IdentificationLockModal.tsx b/ts/screens/modal/IdentificationLockModal.tsx index 3e46caace75..0ffb349fbf5 100644 --- a/ts/screens/modal/IdentificationLockModal.tsx +++ b/ts/screens/modal/IdentificationLockModal.tsx @@ -1,74 +1,125 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; -import { format } from "date-fns"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import * as React from "react"; -import { View, Image, Modal, StyleSheet } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import errorIcon from "../../../img/messages/error-message-detail-icon.png"; -import { H1 } from "../../components/core/typography/H1"; +import { useEffect } from "react"; +import { View, Modal, StyleSheet, SafeAreaView } from "react-native"; +import { + ContentWrapper, + LabelSmall, + Pictogram, + VSpacer +} from "@pagopa/io-app-design-system"; import { H3 } from "../../components/core/typography/H3"; import { IOStyles } from "../../components/core/variables/IOStyles"; import I18n from "../../i18n"; +import { useIODispatch } from "../../store/hooks"; +import { identificationHideLockModal } from "../../store/actions/identification"; +import { IOStyleVariables } from "../../components/core/variables/IOStyleVariables"; +import { + CountdownProvider, + useCountdown +} from "../../components/countdown/CountdownProvider"; +import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; +import { ProgressIndicator } from "../../components/ui/ProgressIndicator"; type Props = { - // milliseconds - countdown?: Millisecond; + countdownInMs: Millisecond; + timeSpanInSeconds: number; }; -const styles = StyleSheet.create({ - imageContainer: { - paddingTop: 96 - }, - spaced: { - flexDirection: "column", - alignItems: "center" - } -}); - -const wrongCodeText = I18n.t("global.genericRetry"); const waitMessageText = I18n.t("identification.fail.waitMessage"); const tooManyAttemptsText = I18n.t("identification.fail.tooManyAttempts"); -// Convert milliseconds to a textual representation based on mm:ss +const TIMER_INTERVAL = 1000; -const fromMillisecondsToTimeRepresentation = (ms: Millisecond): string => - format(new Date(ms), "mm:ss"); +type CountdownProps = { + onElapsedTimer: () => void; + timeSpanInSeconds: number; +}; + +const Countdown = (props: CountdownProps) => { + const { timerCount, startTimer } = useCountdown(); + const { onElapsedTimer, timeSpanInSeconds } = props; + const loaderValue = Math.round((timerCount * 100) / timeSpanInSeconds); + + useOnFirstRender(() => { + startTimer?.(); + }); + + useEffect(() => { + if (timerCount === 0) { + onElapsedTimer(); + } + }, [onElapsedTimer, timerCount]); + + return ( + <> + + + + + {waitMessageText} + + {timerCount}s + + + ); +}; /* This modal screen is displayed when too many wrong pin attempts have been made. A countdown is displayed indicating how long it is to unlock the application. */ +export const IdentificationLockModal = (props: Props) => { + const { countdownInMs, timeSpanInSeconds } = props; + const timerTiming = Math.round((countdownInMs as number) / 1000); -export const IdentificationLockModal: React.FunctionComponent< - Props -> = props => { - const minuteSeconds = pipe( - props.countdown, - O.fromNullable, - O.fold( - () => "0:00", - x => fromMillisecondsToTimeRepresentation(x) - ) - ); + const dispatch = useIODispatch(); + const hideModal = React.useCallback(() => { + dispatch(identificationHideLockModal()); + }, [dispatch]); return ( - - - - - -

{wrongCodeText}

- - -

{tooManyAttemptsText}

-

{waitMessageText}

-
- -

{minuteSeconds}

- -
+ + + + + +

+ {tooManyAttemptsText} +

+ + + + +
+
+
); }; + +const styles = StyleSheet.create({ + container: { + ...IOStyles.bgWhite, + ...IOStyles.centerJustified, + ...IOStyles.flex + }, + contentTitle: { + textAlign: "center" + }, + content: { + alignItems: "center" + } +}); diff --git a/ts/screens/modal/IdentificationModal.tsx b/ts/screens/modal/IdentificationModal.tsx index 3f19cf662f6..cee551ed07e 100644 --- a/ts/screens/modal/IdentificationModal.tsx +++ b/ts/screens/modal/IdentificationModal.tsx @@ -1,15 +1,24 @@ -import { Millisecond } from "@pagopa/ts-commons/lib/units"; +import * as React from "react"; +import { useCallback, useState, useRef, useMemo, memo } from "react"; +import { + H2, + VSpacer, + Pictogram, + IconButton, + ContentWrapper, + ButtonLink, + IOStyles, + ToastNotification, + IOPictograms +} from "@pagopa/io-app-design-system"; +import _ from "lodash"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import { Content } from "native-base"; -import * as React from "react"; -import { View, Alert, Modal, StatusBar, StyleSheet, Text } from "react-native"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import { Link } from "../../components/core/typography/Link"; -import Pinpad from "../../components/Pinpad"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; +import { Alert, ColorSchemeName, Modal, View, StyleSheet } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { ScrollView } from "react-native-gesture-handler"; +import { useDispatch } from "react-redux"; +import { Millisecond } from "@pagopa/ts-commons/lib/units"; import I18n from "../../i18n"; import { identificationCancel, @@ -18,670 +27,412 @@ import { identificationPinReset, identificationSuccess } from "../../store/actions/identification"; -import { appCurrentStateSelector } from "../../store/reducers/appState"; -import { assistanceToolConfigSelector } from "../../store/reducers/backendStatus"; +import { useIOSelector } from "../../store/hooks"; import { - freeAttempts, IdentificationCancelData, identificationFailSelector, maxAttempts, progressSelector } from "../../store/reducers/identification"; -import { isFingerprintEnabledSelector } from "../../store/reducers/persistedPreferences"; +import { useBiometricType } from "../../utils/hooks/useBiometricType"; import { profileNameSelector } from "../../store/reducers/profile"; -import { GlobalState } from "../../store/reducers/types"; +import { biometricAuthenticationRequest } from "../../utils/biometrics"; +import { appCurrentStateSelector } from "../../store/reducers/appState"; +import { usePrevious } from "../../utils/hooks/usePrevious"; import { setAccessibilityFocus } from "../../utils/accessibility"; import { - biometricAuthenticationRequest, - BiometricsValidType, - getBiometricsType, - isBiometricsValidType -} from "../../utils/biometrics"; -import { maybeNotNullyString } from "../../utils/strings"; - -import customVariables from "../../theme/variables"; - -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { Label } from "../../components/core/typography/Label"; -import { Body } from "../../components/core/typography/Body"; -import { H2 } from "../../components/core/typography/H2"; + FAIL_ATTEMPTS_TO_SHOW_ALERT, + IdentificationInstructionsComponent, + getBiometryIconName +} from "../../utils/identification"; +import { useAppBackgroundAccentColorName } from "../../utils/hooks/theme"; import { IdentificationLockModal } from "./IdentificationLockModal"; +import { IdentificationNumberPad } from "./components/IdentificationNumberPad"; -type Props = ReturnType & - ReturnType; - -/** - * Type used in the local state to save the result of Pinpad code matching. - * State is "unstarted" if the user still need to insert the unlock code. - * State is "failure" when the unlock code inserted by the user do not match the - * stored one. - */ -type IdentificationByPinState = "unstarted" | "failure"; - -type IdentificationByBiometryState = "unstarted" | "failure"; - -type State = { - identificationByPinState: IdentificationByPinState; - identificationByBiometryState: IdentificationByBiometryState; - biometryType?: BiometricsValidType; - biometryAuthAvailable: boolean; - canInsertPinTooManyAttempts: boolean; - countdown?: Millisecond; - errorDescription?: string; -}; - -const checkPinInterval = 100 as Millisecond; - -// the threshold of attempts after which it is necessary to activate the timer check -const checkTimerThreshold = maxAttempts - freeAttempts; +const VERTICAL_PADDING = 16; +const A11Y_FOCUS_DELAY = 1000 as Millisecond; const onRequestCloseHandler = () => undefined; -const styles = StyleSheet.create({ - bottomContainer: { - position: "absolute", - bottom: 20, - alignSelf: "center" - }, - contentContainerStyle: { - flexGrow: 1, - padding: customVariables.contentPadding - } -}); - -/** - * A component used to identify the the user. - * The identification process can be activated calling a saga or dispatching the - * identificationRequest redux action. - * The modal can have 2 designs: - * 1. primary background: used to autenticate the user when he/she enters the app - * 2. white background: used to identify the user when he/she wants to complete a task (eg a payment) - * The 2nd design is displayed when isValidatingTask (from the identificationProgressState) is true - */ -class IdentificationModal extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - identificationByPinState: "unstarted", - identificationByBiometryState: "unstarted", - biometryAuthAvailable: true, - canInsertPinTooManyAttempts: O.isNone(this.props.identificationFailState) - }; +// eslint-disable-next-line sonarjs/cognitive-complexity, complexity +const IdentificationModal = () => { + const [isBiometricLocked, setIsBiometricLocked] = useState(false); + const showRetryText = useRef(false); + const headerRef = useRef(null); + const errorStatusRef = useRef(null); + const colorScheme: ColorSchemeName = "light"; + const numberPadVariant = colorScheme ? "dark" : "light"; + + const blueColor = useAppBackgroundAccentColorName(); + + const appState = useIOSelector(appCurrentStateSelector); + const previousAppState = usePrevious(appState); + const identificationProgressState = useIOSelector(progressSelector); + const previousIdentificationProgressState = usePrevious( + identificationProgressState + ); + const identificationFailState = useIOSelector( + identificationFailSelector, + // Since the identificationFailState is an Option, + // we need to performs a deep comparison between + // two values to determine if they are equivalent + // to avoid unnecessary re-renders. + (l, r) => _.isEqual(l, r) + ); + const name = useIOSelector(profileNameSelector); + const { biometricType, isFingerprintEnabled } = useBiometricType(); + + // eslint-disable-next-line functional/no-let + let pin = ""; + // eslint-disable-next-line functional/no-let + let isValidatingTask = false; + // eslint-disable-next-line functional/no-let + let cancelData: IdentificationCancelData | undefined; + if (identificationProgressState.kind === "started") { + pin = identificationProgressState.pin; + isValidatingTask = identificationProgressState.isValidatingTask; + const { identificationCancelData } = identificationProgressState; + cancelData = identificationCancelData; } - private headerRef = React.createRef(); - private errorStatusRef = React.createRef(); - - private idUpdateCanInsertPinTooManyAttempts?: number; - - /** - * Update the state using the actual props value of the `identificationFailState` - * return the updated value of `canInsertPinTooManyAttempts` in order to be used without waiting the state update - */ - private updateCanInsertPinTooManyAttempts = () => - pipe( - this.props.identificationFailState, - O.map(errorData => { - const now = new Date(); - const canInsertPinTooManyAttempts = errorData.nextLegalAttempt <= now; - this.setState({ - canInsertPinTooManyAttempts, - countdown: (errorData.nextLegalAttempt.getTime() - - now.getTime()) as Millisecond - }); - return canInsertPinTooManyAttempts; - }) - ); - - /** - * Activate the interval check on the pin state if the condition is satisfied - */ - private scheduleCanInsertPinUpdate = () => { - pipe( - this.props.identificationFailState, - O.map(failState => { - if (failState.remainingAttempts < checkTimerThreshold) { - this.updateCanInsertPinTooManyAttempts(); - // eslint-disable-next-line - this.idUpdateCanInsertPinTooManyAttempts = setInterval( - this.updateCanInsertPinTooManyAttempts, - checkPinInterval - ); - } - }) + const forgotCodeLabel = `${I18n.t( + "identification.unlockCode.reset.button" + )} ${I18n.t("identification.unlockCode.reset.code")}?`; + const closeButtonLabel = cancelData?.label ?? I18n.t("global.buttons.close"); + const textTryAgain = I18n.t("global.genericRetry"); + + const dispatch = useDispatch(); + const onIdentificationCancel = useCallback(() => { + dispatch(identificationCancel()); + }, [dispatch]); + const onIdentificationSuccess = useCallback( + (isBiometric: boolean) => { + dispatch(identificationSuccess({ isBiometric })); + }, + [dispatch] + ); + const onIdentificationForceLogout = useCallback(() => { + dispatch(identificationForceLogout()); + }, [dispatch]); + const onIdentificationFailure = useCallback(() => { + dispatch(identificationFailure()); + }, [dispatch]); + + const onIdentificationFailureHandler = useCallback(() => { + const forceLogout = pipe( + identificationFailState, + O.map(failState => failState.remainingAttempts === 1), + O.getOrElse(() => false) ); - }; - - public componentDidMount() { - const { isFingerprintEnabled } = this.props; - setAccessibilityFocus(this.headerRef); - if (isFingerprintEnabled) { - getBiometricsType().then( - biometricsType => - this.setState({ - biometryType: isBiometricsValidType(biometricsType) - ? biometricsType - : undefined - }), - _ => 0 - ); + if (forceLogout) { + onIdentificationForceLogout(); } else { - // if the biometric is not available unlock the unlock code insertion - this.setState({ biometryAuthAvailable: false }); + onIdentificationFailure(); } + }, [ + identificationFailState, + onIdentificationFailure, + onIdentificationForceLogout + ]); + + const onIdentificationSuccessHandler = useCallback( + (isBiometric: boolean) => { + if (identificationProgressState.kind !== "started") { + return; + } - // first time the component is mounted, need to calculate the state value for `canInsertPinTooManyAttempts` - // and schedule the update if needed - pipe( - this.updateCanInsertPinTooManyAttempts(), - O.map(_ => this.scheduleCanInsertPinUpdate()) - ); - } - - // atm this method is never called because the component won't be never unmount - public componentWillUnmount() { - clearInterval(this.idUpdateCanInsertPinTooManyAttempts); - } - - /** - * Check if fingerprint login can be prompted by looking at three parameters in - * serie: - * 1. The current state of identification process - * 2. `isFingerprintEnabled` whose value comes from app preferences - * 3. Current status of biometry recognition system, provided by querying - * the library in charge. - * - * @param {boolean} updateBiometrySupportProp – This flag is needed because - * this funciton can be run from several contexts: when it is called while - * the app is returning foreground from background, biometry support status - * has to be updated in case of system preferences changes. - */ - private maybeTriggerFingerprintRequest(updateBiometrySupportProp?: { - updateBiometrySupportProp: boolean; - }) { - // check if the state of identification process is correct - const { identificationProgressState, isFingerprintEnabled } = this.props; - - if (identificationProgressState.kind !== "started") { - return; - } - // Check for global properties to know if biometric recognition is enabled - if (isFingerprintEnabled) { - getBiometricsType() - .then( - biometricsType => { - if (updateBiometrySupportProp) { - this.setState({ - biometryType: isBiometricsValidType(biometricsType) - ? biometricsType - : undefined, - biometryAuthAvailable: isBiometricsValidType(biometricsType) - }); - } - }, - _ => undefined - ) - .then( - () => { - if (this.state.biometryType) { - void this.onFingerprintRequest( - this.onIdentificationSuccessHandler - ); - } - }, - _ => undefined - ); - } - } - - public componentDidUpdate(prevProps: Props, prevState: State) { - // When app becomes active from background the state of TouchID support - // must be updated, because it might be switched off. - // Don't do this check if I can't authenticate for too many attempts (canInsertPinTooManyAttempts === false) - if ( - this.state.canInsertPinTooManyAttempts && - ((prevProps.appState === "background" && - this.props.appState === "active") || - (prevProps.identificationProgressState.kind !== "started" && - this.props.identificationProgressState.kind === "started")) - ) { - this.maybeTriggerFingerprintRequest({ - updateBiometrySupportProp: - prevProps.appState !== "active" && this.props.appState === "active" - }); - setAccessibilityFocus(this.headerRef); - } - - // Added to solve the issue https://www.pivotaltracker.com/story/show/173217033 - if (prevProps.isFingerprintEnabled !== this.props.isFingerprintEnabled) { - this.setState((_, props) => ({ - biometryAuthAvailable: pipe( - props.isFingerprintEnabled, - O.fromNullable, - O.getOrElse(() => false) - ) - })); - } - - const previousAttempts = pipe( - prevProps.identificationFailState, - O.fold( - () => Number.MAX_VALUE, - x => x.remainingAttempts - ) - ); - - const currentAttempts = pipe( - this.props.identificationFailState, - O.fold( - () => Number.MAX_VALUE, - x => x.remainingAttempts - ) - ); - - // trigger an update in the management of the updateInterval if the attempts or the state - // `canInsertPinTooManyAttempts` is changed - if ( - previousAttempts !== currentAttempts || - prevState.canInsertPinTooManyAttempts !== - this.state.canInsertPinTooManyAttempts - ) { - // trigger a state update based on the current props and use the results to choose what to do - // with the scheduled interval - const caninsertPin = pipe( - this.updateCanInsertPinTooManyAttempts(), - O.getOrElse(() => true) - ); - // if the pin can be inserted, the timer is no longer needed - if (caninsertPin) { - clearInterval(this.idUpdateCanInsertPinTooManyAttempts); - // eslint-disable-next-line - this.idUpdateCanInsertPinTooManyAttempts = undefined; + const { identificationSuccessData } = identificationProgressState; - // if the pin can't be inserted and is not scheduled an interval, schedule an update - } else if (this.idUpdateCanInsertPinTooManyAttempts === undefined) { - this.scheduleCanInsertPinUpdate(); + if (identificationSuccessData) { + identificationSuccessData.onSuccess(); } - } - } - - private onIdentificationSuccessHandler = () => { - const { identificationProgressState } = this.props; + onIdentificationSuccess(isBiometric); + }, + [identificationProgressState, onIdentificationSuccess] + ); + const onIdentificationCancelHandler = useCallback(() => { if (identificationProgressState.kind !== "started") { return; } - // The identification state is started we need to show the modal - const { identificationSuccessData } = identificationProgressState; + const { identificationCancelData } = identificationProgressState; - if (identificationSuccessData) { - identificationSuccessData.onSuccess(); - } - this.props.onIdentificationSuccess(); - }; - - private onIdentificationFailureHandler = () => { - const { identificationFailState } = this.props; + identificationCancelData?.onCancel(); + onIdentificationCancel(); + }, [identificationProgressState, onIdentificationCancel]); - const forceLogout = pipe( - identificationFailState, - O.map(failState => failState.remainingAttempts === 1), - O.getOrElse(() => false) - ); - if (forceLogout) { - this.props.onIdentificationForceLogout(); - } else { - this.props.onIdentificationFailure(); - } - }; - - private loadAlertTexts = ( - profileName: string | undefined - ): [string, string] => - pipe( - profileName, - O.fromNullable, - O.fold( - () => [ - I18n.t("identification.logout"), - I18n.t("identification.logoutDescription") - ], - pn => [ - I18n.t("identification.logoutProfileName", { - profileName: pn - }), - I18n.t("identification.logoutDescriptionProfileName", { - profileName: pn - }) - ] - ) - ); - - private onLogout = () => { - Alert.alert( - ...this.loadAlertTexts(this.props.profileName), - [ - { - text: I18n.t("global.buttons.cancel"), - style: "cancel" + const onFingerprintRequest = useCallback( + () => + biometricAuthenticationRequest( + () => { + onIdentificationSuccessHandler(true); }, - { - text: I18n.t("global.buttons.continue"), - onPress: this.props.onIdentificationForceLogout + e => { + if (e.name === "DeviceLocked" || e.name === "DeviceLockedPermanent") { + setIsBiometricLocked(true); + } } - ], - { cancelable: true } - ); - }; - - // The generic "Try again" message appears if you make a mistake - // in entering the unlock code or in case of failed biometric authentication. - // If you fail to write the unlock code more than 4 times you should see - // "Try again. Number of remaining attempts". In case of failed biometric identification - // the message is displayed when you open the app by holding the "wrong finger" on the biometric iPhone sensor. - private getErrorText = () => { - const textTryAgain = I18n.t("global.genericRetry"); - - if (this.state.identificationByPinState === "failure") { - this.setState({ - identificationByBiometryState: "unstarted" - }); - return pipe( - this.props.identificationFailState, - O.filter(fs => fs.remainingAttempts <= maxAttempts - freeAttempts), - O.map( - // here if the user finished his free attempts - fd => - `${textTryAgain}. ${I18n.t( - fd.remainingAttempts > 1 - ? "identification.fail.remainingAttempts" - : "identification.fail.remainingAttemptSingle", - { attempts: fd.remainingAttempts } - )}` + ), + [onIdentificationSuccessHandler, setIsBiometricLocked] + ); + + const biometricsConfig = useMemo( + () => + biometricType + ? { + biometricType, + biometricAccessibilityLabel: getBiometryIconName(biometricType), + onBiometricPress: () => onFingerprintRequest() + } + : {}, + [biometricType, onFingerprintRequest] + ); + + const onPinResetHandler = useCallback(() => { + dispatch(identificationPinReset()); + }, [dispatch]); + + const confirmResetAlert = useCallback( + () => + Alert.alert( + I18n.t("identification.forgetCode.confirmTitle"), + I18n.t( + isValidatingTask + ? "identification.forgetCode.confirmMsgWithTask" + : "identification.forgetCode.confirmMsg" ), - O.getOrElse(() => textTryAgain) - ); + [ + { + text: I18n.t("global.buttons.confirm"), + style: "default", + onPress: onPinResetHandler + }, + { + text: I18n.t("global.buttons.cancel"), + style: "cancel" + } + ], + { cancelable: false } + ), + [isValidatingTask, onPinResetHandler] + ); + + const titleLabel = isValidatingTask + ? I18n.t("identification.titleValidation") + : name + ? I18n.t("identification.title", { name }) + : ""; + + const onPinValidated = useCallback( + (isValidated: boolean) => { + if (isValidated) { + // eslint-disable-next-line functional/immutable-data + showRetryText.current = false; + onIdentificationSuccessHandler(false); + } else { + // eslint-disable-next-line functional/immutable-data + showRetryText.current = true; + onIdentificationFailureHandler(); + } + }, + [onIdentificationFailureHandler, onIdentificationSuccessHandler] + ); + + const NumberPad = memo(() => ( + + )); + + const pictogramKey: IOPictograms = isValidatingTask ? "passcode" : "key"; + + // Managing the countdown and the remaining attempts + // this way is simpler and more predictable + // instead of using useEffect and ref and state. + // eslint-disable-next-line functional/no-let + let countdownInMs = 0 as Millisecond; + // eslint-disable-next-line functional/no-let + let timeSpanBetweenAttemptsInSeconds = 0; + // eslint-disable-next-line functional/no-let + let showLockModal = false; + // eslint-disable-next-line functional/no-let + let remainingAttempts = maxAttempts; + if (O.isSome(identificationFailState)) { + showLockModal = identificationFailState.value.showLockModal ?? false; + remainingAttempts = identificationFailState.value.remainingAttempts; + timeSpanBetweenAttemptsInSeconds = + identificationFailState.value.timespanBetweenAttempts; + const nowInMs = new Date().getTime(); + const nextLegalAttemptInMs = + identificationFailState.value.nextLegalAttempt.getTime(); + const elapsedTimeInMs = nextLegalAttemptInMs - nowInMs; + // This screen is refreshing at every app state change. + // So we can rely on the elapsed time to show the lock modal. + if (showLockModal && elapsedTimeInMs > 0) { + // We need to show the lock modal with the countdown + // updated with the remaining time to handle cases where + // the app has been killed and restarted. + // eslint-disable-next-line functional/immutable-data + countdownInMs = elapsedTimeInMs as Millisecond; } - - if (this.state.identificationByBiometryState === "failure") { - this.setState({ - identificationByPinState: "unstarted" - }); - return textTryAgain; + } + const remainingAttemptsText = I18n.t( + remainingAttempts > 1 + ? "identification.fail.remainingAttempts" + : "identification.fail.remainingAttemptSingle", + { + attempts: remainingAttempts } + ); - return undefined; - }; - - private renderErrorDescription = () => - pipe( - maybeNotNullyString(this.getErrorText()), - O.fold( - () => undefined, - des => ( - - - - ) - ) - ); - - /** - * Create handlers merging default internal actions (to manage the identification state) - * with, if available, custom actions passed as props. - */ - private onIdentificationCancelHandler( - identificationCancelData: IdentificationCancelData - ) { - identificationCancelData.onCancel(); - this.props.onCancelIdentification(); + // If the authentication process is not started, we don't show the modal. + // We need to put this before the biometric request, + // to avoid the biometric request to be triggered when the modal is not shown. + if (identificationProgressState.kind !== "started") { + return null; } - private renderHeader(isValidatingTask: boolean) { - return ( - -

- {isValidatingTask - ? I18n.t("identification.titleValidation") - : pipe( - this.props.profileName, - O.fromNullable, - O.fold( - () => I18n.t("identification.title"), - pN => - I18n.t("identification.titleProfileName", { - profileName: pN - }) - ) - )} -

- - {this.getInstructions()} - -
- ); + // When app becomes active from background the state of TouchID support + // must be updated, because it might be switched off. + // Don't do this check if I can't authenticate + // for too many attempts. + if ( + !showLockModal && + isFingerprintEnabled && + ((previousAppState === "background" && appState === "active") || + (previousIdentificationProgressState?.kind !== "started" && + identificationProgressState.kind === "started")) + ) { + void onFingerprintRequest(); } - public render() { - const { identificationProgressState, isFingerprintEnabled } = this.props; - - if (identificationProgressState.kind !== "started") { - return null; - } - - // The identification is started, we need to show the modal - const { pin, isValidatingTask, identificationCancelData, shufflePad } = - identificationProgressState; - - const { biometryType, countdown, identificationByBiometryState } = - this.state; + const remainingAttemptsToShowAlert = + remainingAttempts <= FAIL_ATTEMPTS_TO_SHOW_ALERT; + const showToastNotificationAlert = + remainingAttemptsToShowAlert || showRetryText.current; - const canInsertPin = - !this.state.biometryAuthAvailable && - this.state.canInsertPinTooManyAttempts; - - // display the remaining attempts number only if start to lock the application for too many attempts - const displayRemainingAttempts = pipe( - this.props.identificationFailState, - O.fold( - () => undefined, - failState => - failState.remainingAttempts <= maxAttempts - freeAttempts - ? failState.remainingAttempts - : undefined - ) - ); - - const defaultColor = isValidatingTask - ? customVariables.contentPrimaryBackground - : IOColors.white; + if (showRetryText.current) { + setAccessibilityFocus(errorStatusRef, A11Y_FOCUS_DELAY); + } - return !this.state.canInsertPinTooManyAttempts ? ( - IdentificationLockModal({ countdown }) - ) : ( - - + ) : ( + + + {isValidatingTask && ( + + + + { + onIdentificationCancelHandler(); + }} + accessibilityLabel={closeButtonLabel} + /> + + + )} + - - - {this.renderHeader(isValidatingTask)} - {this.renderErrorDescription()} - - this.onFingerprintRequest(this.onIdentificationSuccessHandler) - } - shufflePad={shufflePad} - disabled={!canInsertPin} - compareWithCode={pin as string} - activeColor={defaultColor} - inactiveColor={defaultColor} - buttonType={isValidatingTask ? "light" : "primary"} - delayOnFailureMillis={1000} - onFulfill={(_: string, __: boolean) => - this.onPinFullfill( - _, - __, - this.onIdentificationSuccessHandler, - this.onIdentificationFailureHandler - ) - } - clearOnInvalid={true} - onCancel={ - identificationCancelData - ? () => - this.onIdentificationCancelHandler( - identificationCancelData - ) - : undefined - } - remainingAttempts={displayRemainingAttempts} - /> - - {!isValidatingTask && ( - - - {pipe( - this.props.profileName, - O.fromNullable, - O.fold( - () => I18n.t("identification.logout"), - pN => - I18n.t("identification.logoutProfileName", { - profileName: pN - }) - ) - )} - + + + + {showToastNotificationAlert ? ( + + + + ) : ( + + + + )} + + +

{titleLabel}

+ +
- )} -
-
-
- ); - } - - /** - * Return the proper instruction based on the avaiable identification method - */ - private getInstructions(): string { - // We have a failure cause the biometry auth responded with a DeviceLocked or DeviceLockedPermanent code. - // message should not include biometry instructions - if (this.state.identificationByBiometryState === "failure") { - return I18n.t("identification.subtitleCode"); - } +
+ + + + + + + + + + + + + + ); +}; - switch (this.state.biometryType) { - case "BIOMETRICS": - return I18n.t("identification.subtitleCodeFingerprint"); - case "FACE_ID": - return I18n.t("identification.subtitleCodeFaceId"); - case "TOUCH_ID": - return I18n.t("identification.subtitleCodeFingerprint"); - default: - return I18n.t("identification.subtitleCode"); - } +const styles = StyleSheet.create({ + safeArea: { flexGrow: 1 }, + closeButton: { + zIndex: 100, + flexGrow: 1, + alignItems: "flex-end" + }, + scrollViewContentContainer: { + flexGrow: 1 + }, + alertContainer: { + flexGrow: 1 + }, + smallPinLabel: { + position: "absolute", + alignItems: "center", + opacity: 0.5, + bottom: -32 } - - private onPinFullfill = ( - _: string, - isValid: boolean, - onIdentificationSuccessHandler: () => void, - onIdentificationFailureHandler: () => void - ) => { - if (isValid) { - this.setState({ - identificationByPinState: "unstarted" - }); - onIdentificationSuccessHandler(); - } else { - this.setState( - { - identificationByPinState: "failure" - }, - () => setAccessibilityFocus(this.errorStatusRef) - ); - - onIdentificationFailureHandler(); - } - }; - - private onFingerprintRequest = (onIdentificationSuccessHandler: () => void) => - biometricAuthenticationRequest( - () => { - this.setState({ - identificationByBiometryState: "unstarted" - }); - onIdentificationSuccessHandler(); - }, - e => { - // some error occured, enable pin insertion - this.setState({ - biometryAuthAvailable: false - }); - if (e.name === "DeviceLocked" || e.name === "DeviceLockedPermanent") { - this.setState({ - identificationByBiometryState: "failure" - }); - } - } - ); -} - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onPinResetHandler: () => dispatch(identificationPinReset()), - onCancelIdentification: () => dispatch(identificationCancel()), - onIdentificationSuccess: () => dispatch(identificationSuccess()), - onIdentificationForceLogout: () => dispatch(identificationForceLogout()), - onIdentificationFailure: () => dispatch(identificationFailure()) -}); - -const mapStateToProps = (state: GlobalState) => ({ - identificationProgressState: progressSelector(state), - identificationFailState: identificationFailSelector(state), - isFingerprintEnabled: isFingerprintEnabledSelector(state), - appState: appCurrentStateSelector(state), - profileName: profileNameSelector(state), - assistanceToolConfig: assistanceToolConfigSelector(state) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(IdentificationModal); +export default IdentificationModal; diff --git a/ts/screens/modal/RootedDeviceModal.tsx b/ts/screens/modal/RootedDeviceModal.tsx index 58f12392198..dc00382d374 100644 --- a/ts/screens/modal/RootedDeviceModal.tsx +++ b/ts/screens/modal/RootedDeviceModal.tsx @@ -1,164 +1,106 @@ -import { Container, Content } from "native-base"; import * as React from "react"; +import { View, Platform, StyleSheet } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useDispatch } from "react-redux"; import { - View, - Alert, - AlertButton, - Image, - Platform, - SafeAreaView, - StyleSheet -} from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import image from "../../../img/rooted/broken-phone.png"; -import { H2 } from "../../components/core/typography/H2"; + ButtonLink, + ButtonOutline, + ContentWrapper, + H3, + LabelSmall, + Pictogram, + VSpacer +} from "@pagopa/io-app-design-system"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import { withLoadingSpinner } from "../../components/helpers/withLoadingSpinner"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import { BlockButtonProps } from "../../components/ui/BlockButtons"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; -import Markdown from "../../components/ui/Markdown"; import I18n from "../../i18n"; -import customVariables from "../../theme/variables"; +import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { continueWithRootOrJailbreak } from "../../store/actions/persistedPreferences"; +import { useIOBottomSheetAutoresizableModal } from "../../utils/hooks/bottomSheet"; import { trackLoginRootedScreen } from "./analytics"; -type Props = { - onContinue: () => void; - onCancel: () => void; -}; - -const styles = StyleSheet.create({ - flex: { - flex: 1 - }, - main: { - paddingTop: customVariables.contentPadding, - paddingHorizontal: customVariables.contentPadding, - flex: 1, - alignItems: "center", - justifyContent: "center" - }, - image: { - width: 66, - height: 104 - } -}); - -const CSS_STYLE = ` -body { - text-align: center; -} -`; - -const opacity = 0.9; - -type ConfirmConfig = { - title: string; - body?: string; - confirmText: string; - cancelText?: string; - onConfirmAction: () => void; -}; - -const RootedDeviceModal: React.FunctionComponent = (props: Props) => { - const [markdownLoaded, setMarkdownLoaded] = React.useState(false); +const RootedDeviceModal = () => { trackLoginRootedScreen(); - const showAlert = (confirmConfig: ConfirmConfig) => { - const buttons: ReadonlyArray = [ - { - text: confirmConfig.cancelText - }, - { - text: confirmConfig.confirmText, - onPress: confirmConfig.onConfirmAction, - style: "cancel" - } - ]; - Alert.alert( - confirmConfig.title, - confirmConfig.body ? confirmConfig.body : "", - buttons.slice(confirmConfig.cancelText ? 0 : 1), // remove cancel button if cancelText is undefined - { cancelable: true } - ); - }; - - const continueAlertConfig: ConfirmConfig = { - title: I18n.t("rooted.continueAlert.title"), - body: I18n.t("rooted.continueAlert.body"), - confirmText: I18n.t("rooted.continueAlert.confirmText"), - cancelText: I18n.t("rooted.continueAlert.cancelText"), - onConfirmAction: props.onContinue - }; + const dispatch = useDispatch(); + const navigation = useIONavigation(); - const cancelAlertConfig: ConfirmConfig = { - title: I18n.t("rooted.cancelAlert.title"), - body: I18n.t("rooted.cancelAlert.body"), - confirmText: I18n.t("rooted.cancelAlert.confirmText"), - onConfirmAction: props.onCancel - }; - - const leftButton: BlockButtonProps = { - title: I18n.t("global.buttons.continue"), - bordered: true, - danger: true, - onPress: () => showAlert(continueAlertConfig) - }; - - const rightButton: BlockButtonProps = { - title: I18n.t("global.buttons.cancel"), - primary: true, - onPress: () => showAlert(cancelAlertConfig) - }; - - const onMarkdownLoaded = () => { - setMarkdownLoaded(true); - }; + const handleContinueWithRootOrJailbreak = React.useCallback(() => { + dispatch(continueWithRootOrJailbreak(true)); + navigation.goBack(); + }, [dispatch, navigation]); const body = Platform.select({ ios: I18n.t("rooted.bodyiOS"), default: I18n.t("rooted.bodyAndroid") }); - const ComponentWithLoading = withLoadingSpinner(() => ( - - - - - - - - -

{I18n.t("rooted.title")}

-
-
- - - {body} - -
- -
-
-
- )); + + const { + present: presentLearnMoreBottomSheet, + bottomSheet: learnMoreBottomSheet + } = useIOBottomSheetAutoresizableModal({ + title: I18n.t("rooted.learnMoreBottomsheet.title"), + component: {body} + }); return ( - + + + + + + + + +

{I18n.t("rooted.title")}

+
+ + + + {I18n.t("rooted.body")} + + + + + + + + + + +
+
+ {learnMoreBottomSheet} +
); }; export default RootedDeviceModal; + +const styles = StyleSheet.create({ + flex: { + flex: 1 + }, + textCenter: { + textAlign: "center" + }, + scrollViewContentContainer: { + justifyContent: "center", + alignItems: "center", + flexGrow: 1 + } +}); diff --git a/ts/screens/modal/components/IdentificationNumberPad.tsx b/ts/screens/modal/components/IdentificationNumberPad.tsx new file mode 100644 index 00000000000..a360bb86388 --- /dev/null +++ b/ts/screens/modal/components/IdentificationNumberPad.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import { useState, useCallback } from "react"; +import { + CodeInput, + NumberPad, + VSpacer, + IOStyles, + BiometricsValidType, + IconButton +} from "@pagopa/io-app-design-system"; +import { View } from "react-native"; +import I18n from "../../../i18n"; +import { isDevEnv } from "../../../utils/environment"; + +const PIN_LENGTH = 6; +const CODE_INPUT_ERROR_ANIMATION_DURATION = 500; +const CODE_INPUT_SUCCESS_CALLBACK_CALL_TIMEOUT = 250; + +type BiometricConfigType = + | { + biometricType: BiometricsValidType; + biometricAccessibilityLabel: string; + onBiometricPress: () => Promise; + } + | { + biometricType?: undefined; + biometricAccessibilityLabel?: undefined; + onBiometricPress?: undefined; + }; +type IdentificationNumberPadProps = { + pin: string; + pinValidation: (success: boolean) => void; + numberPadVariant: "light" | "dark"; + biometricsConfig: BiometricConfigType; +}; + +export const IdentificationNumberPad = ( + props: IdentificationNumberPadProps +) => { + const [value, setValue] = useState(""); + + const { pin, pinValidation, numberPadVariant, biometricsConfig } = props; + + const onValueChange = useCallback((v: string) => { + if (v.length <= PIN_LENGTH) { + setValue(v); + } + }, []); + + // Calling pinValidation after a timeout is neeed + // to allow code input to refresh correctly, + // and in case of error to see the shake animation. + const onPinValidated = useCallback( + (v: string) => { + if (v === pin) { + setTimeout(() => { + pinValidation(true); + }, CODE_INPUT_SUCCESS_CALLBACK_CALL_TIMEOUT); + return true; + } else { + setTimeout(() => { + pinValidation(false); + }, CODE_INPUT_ERROR_ANIMATION_DURATION); + return false; + } + }, + [pin, pinValidation] + ); + + // We don't need to handle the value change on code input, + // only on number pad. + const onCodeInputValueChange = useCallback(() => void 0, []); + + return ( + <> + + + + + + + + {isDevEnv && ( + + + { + setValue(pin); + }} + accessibilityLabel={"Insert valid pin button (dev only)"} + /> + + )} + + ); +}; diff --git a/ts/screens/onboarding/OnboardingCompletedScreen.tsx b/ts/screens/onboarding/OnboardingCompletedScreen.tsx index 93f49abb428..dd9afcd7ec3 100644 --- a/ts/screens/onboarding/OnboardingCompletedScreen.tsx +++ b/ts/screens/onboarding/OnboardingCompletedScreen.tsx @@ -1,14 +1,7 @@ import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { SafeAreaView } from "react-native"; +import React from "react"; import { useDispatch } from "react-redux"; -import { Pictogram } from "@pagopa/io-app-design-system"; import { useRoute } from "@react-navigation/native"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { InfoScreenComponent } from "../../components/infoScreen/InfoScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; -import { BlockButtonProps } from "../../components/ui/BlockButtons"; import I18n from "../../i18n"; import { completeOnboarding } from "../../store/actions/onboarding"; import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; @@ -18,6 +11,7 @@ import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selec import { idpSelector } from "../../store/reducers/authentication"; import { trackLoginEnded } from "../authentication/analytics"; import { getFlowType } from "../../utils/analytics"; +import { OperationResultScreenContent } from "../../components/screens/OperationResultScreenContent"; const OnboardingCompletedScreen = () => { const dispatch = useDispatch(); @@ -28,38 +22,30 @@ const OnboardingCompletedScreen = () => { const idp = O.isSome(idpSelected) ? idpSelected.value.name : ""; const route = useRoute(); - const continueButtonProps: BlockButtonProps = { - bordered: false, - title: I18n.t("global.buttons.continue"), - onPress: () => { - trackLoginEnded( - isFastLoginEnabled, - idp, - getFlowType(false, true), - route.name - ); - dispatch(completeOnboarding()); - } - }; - useOnFirstRender(() => { trackThankYouPageScreen(); }); - return ( - - - } - title={I18n.t("onboarding.thankYouPage.title")} - /> + const handleContinue = () => { + trackLoginEnded( + isFastLoginEnabled, + idp, + getFlowType(false, true), + route.name + ); + dispatch(completeOnboarding()); + }; - - - + return ( + ); }; diff --git a/ts/screens/onboarding/OnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/OnboardingEmailInsertScreen.tsx deleted file mode 100644 index a4c205799dd..00000000000 --- a/ts/screens/onboarding/OnboardingEmailInsertScreen.tsx +++ /dev/null @@ -1,298 +0,0 @@ -/** - * A screen where user after login (with CIE) can set email address if it is - * not present in the profile. - */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { EmailString } from "@pagopa/ts-commons/lib/strings"; -import * as E from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { Content, Form } from "native-base"; -import * as React from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { View, Alert, Keyboard, SafeAreaView, StyleSheet } from "react-native"; -import { StackActions } from "@react-navigation/native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { H1 } from "../../components/core/typography/H1"; -import { LabelledItem } from "../../components/LabelledItem"; -import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; -import BaseScreenComponent, { - ContextualHelpPropsMarkdown -} from "../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; -import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { OnboardingParamsList } from "../../navigation/params/OnboardingParamsList"; -import ROUTES from "../../navigation/routes"; -import { abortOnboarding, emailInsert } from "../../store/actions/onboarding"; -import { profileLoadRequest, profileUpsert } from "../../store/actions/profile"; -import { useIODispatch, useIOSelector } from "../../store/hooks"; -import { - profileEmailSelector, - profileSelector -} from "../../store/reducers/profile"; -import { usePrevious } from "../../utils/hooks/usePrevious"; -import { withKeyboard } from "../../utils/keyboard"; -import { areStringsEqual } from "../../utils/options"; -import { showToast } from "../../utils/showToast"; -import { Body } from "../../components/core/typography/Body"; -import { IOStyles } from "../../components/core/variables/IOStyles"; - -type Props = IOStackNavigationRouteProps< - OnboardingParamsList, - "ONBOARDING_INSERT_EMAIL_SCREEN" ->; - -const styles = StyleSheet.create({ - flex: { - flex: 1 - } -}); - -const EMPTY_EMAIL = ""; - -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "email.insert.help.title", - body: "email.insert.help.content" -}; - -/** - * A screen to allow user to insert an email address. - */ -const OnboardingEmailInsertScreen = (props: Props) => { - const dispatch = useIODispatch(); - - const profile = useIOSelector(profileSelector); - const optionEmail = useIOSelector(profileEmailSelector); - const isLoading = useMemo( - () => pot.isUpdating(profile) || pot.isLoading(profile), - [profile] - ); - - const updateEmail = useCallback( - (email: EmailString) => - dispatch( - profileUpsert.request({ - email - }) - ), - [dispatch] - ); - const acknowledgeEmailInsert = useCallback( - () => dispatch(emailInsert()), - [dispatch] - ); - const requestAbortOnboarding = useCallback( - () => dispatch(abortOnboarding()), - [dispatch] - ); - const reloadProfile = useCallback( - () => dispatch(profileLoadRequest()), - [dispatch] - ); - - const [isMounted, setIsMounted] = useState(true); - const [email, setEmail] = useState(optionEmail); - - /** validate email returning three possible values: - * - _true_, if email is valid. - * - _false_, if email has been already changed from the user and it is not - * valid. - * - _undefined_, if email field is empty. This state is consumed by - * LabelledItem Component and it used for style pourposes ONLY. - */ - const isValidEmail = () => - pipe( - email, - O.map(value => { - if (EMPTY_EMAIL === value) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ); - - const continueOnPress = () => { - Keyboard.dismiss(); - if (isValidEmail()) { - // The profile is reloaded to check if the user email - // has been updated within another session - reloadProfile(); - } - }; - - const renderFooterButtons = () => { - const continueButtonProps = { - disabled: isValidEmail() !== true && !isLoading, - onPress: continueOnPress, - title: I18n.t("global.buttons.continue"), - block: true, - primary: isValidEmail() - }; - - return ( - - ); - }; - - const handleOnChangeEmailText = (value: string) => { - setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); - }; - - const navigateToEmailReadScreen = useCallback(() => { - props.navigation.dispatch(StackActions.popToTop()); - props.navigation.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN, - params: { isOnboarding: true } - }); - }, [props.navigation]); - - useEffect(() => { - if (!isMounted) { - navigateToEmailReadScreen(); - } - }, [isMounted, navigateToEmailReadScreen]); - - const handleGoBack = useCallback(() => { - // if the onboarding is not completed and the email is set, force goback with a reset (user could edit his email and go back without saving) - // see https://www.pivotaltracker.com/story/show/171424350 - if (O.isSome(optionEmail)) { - setIsMounted(false); - } else { - // if the user is in onboarding phase, go back has to - // abort login (an user with no email can't access the home) - Alert.alert( - I18n.t("onboarding.alert.title"), - I18n.t("onboarding.alert.description"), - [ - { - text: I18n.t("global.buttons.cancel"), - style: "cancel" - }, - { - text: I18n.t("global.buttons.exit"), - style: "default", - onPress: requestAbortOnboarding - } - ] - ); - } - }, [requestAbortOnboarding, optionEmail]); - - const prevUserProfile = usePrevious(profile); - - const prevOptionEmail = usePrevious(optionEmail); - - useEffect(() => { - // When the profile reload is completed, check if the email is changed since the last reload - if ( - prevUserProfile && - pot.isLoading(prevUserProfile) && - !pot.isLoading(profile) - ) { - // Check both if the email has been changed within another session and - // if the inserted email match with the email stored into the user profile - const isTheSameEmail = areStringsEqual(optionEmail, email, true); - if (!isTheSameEmail) { - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - } else { - Alert.alert(I18n.t("email.insert.alert")); - } - } - }, [email, prevUserProfile, profile, optionEmail, updateEmail]); - - useEffect(() => { - if (prevUserProfile && pot.isUpdating(prevUserProfile)) { - if (pot.isError(profile)) { - // display a toast with error - showToast(I18n.t("email.edit.upsert_ko"), "danger"); - } else if (pot.isSome(profile)) { - // user is inserting his email from onboarding phase - // he comes from checkAcknowledgedEmailSaga if onboarding is not finished yet - // and he has not an email - if (prevOptionEmail && O.isNone(prevOptionEmail)) { - // since this screen is mounted from saga it won't be unmounted because on saga - // we have a direct navigation instead of back - // so we have to force a reset (to get this screen unmounted) and navigate to emailReadScreen - // isMounted is used as a guard to prevent update while the screen is unmounting - acknowledgeEmailInsert(); - setIsMounted(false); - return; - } - // go back (to the EmailReadScreen) - handleGoBack(); - return; - } - } - }, [ - acknowledgeEmailInsert, - handleGoBack, - prevOptionEmail, - prevUserProfile, - profile - ]); - - useEffect(() => { - if (prevUserProfile) { - const isPrevCurrentSameState = prevUserProfile.kind === profile.kind; - // do nothing if prev profile is in the same state of the current - if (isMounted || isPrevCurrentSameState) { - return; - } - } - }, [prevUserProfile, profile, isMounted]); - - return ( - - - - - -

- {I18n.t("email.insert.title")} -

- - {I18n.t("email.insert.subtitle")} - -
- EMPTY_EMAIL) - ), - onChangeText: handleOnChangeEmailText - }} - /> - -
-
- - {withKeyboard(renderFooterButtons())} -
-
-
- ); -}; - -export default OnboardingEmailInsertScreen; diff --git a/ts/screens/onboarding/OnboardingEmailReadScreen.tsx b/ts/screens/onboarding/OnboardingEmailReadScreen.tsx deleted file mode 100644 index bcf229f743d..00000000000 --- a/ts/screens/onboarding/OnboardingEmailReadScreen.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * A screen to display the email address used by IO - * The _isFromProfileSection_ navigation parameter let the screen being adapted - * if: - * - it is displayed during the user onboarding - * - it is displayed after the onboarding (navigation from the profile section) - */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as React from "react"; -import { Alert } from "react-native"; -import EmailReadComponent from "../../components/EmailReadComponent"; -import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; -import { TwoButtonsInlineHalf } from "../../components/ui/BlockButtons"; -import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; -import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { OnboardingParamsList } from "../../navigation/params/OnboardingParamsList"; -import ROUTES from "../../navigation/routes"; -import { - abortOnboarding, - emailAcknowledged -} from "../../store/actions/onboarding"; -import { useIODispatch, useIOSelector } from "../../store/hooks"; -import { userMetadataSelector } from "../../store/reducers/userMetadata"; - -type Props = IOStackNavigationRouteProps< - OnboardingParamsList, - "ONBOARDING_READ_EMAIL_SCREEN" ->; - -const OnboardingEmailReadScreen = (props: Props) => { - useValidatedEmailModal(true); - const dispatch = useIODispatch(); - const potUserMetadata = useIOSelector(userMetadataSelector); - - const isLoading = pot.isLoading(potUserMetadata); - - const acknowledgeEmail = () => dispatch(emailAcknowledged()); - const askAbortOnboarding = () => dispatch(abortOnboarding()); - - const handleGoBack = () => { - Alert.alert( - I18n.t("onboarding.alert.title"), - I18n.t("onboarding.alert.description"), - [ - { - text: I18n.t("global.buttons.cancel"), - style: "cancel" - }, - { - text: I18n.t("global.buttons.exit"), - style: "default", - onPress: askAbortOnboarding - } - ] - ); - }; - - const footerProps: TwoButtonsInlineHalf = { - type: "TwoButtonsInlineHalf", - leftButton: { - block: true, - bordered: true, - title: I18n.t("email.edit.cta"), - onPress: () => { - props.navigation.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN - }); - } - }, - rightButton: { - block: true, - primary: true, - title: I18n.t("global.buttons.continue"), - onPress: acknowledgeEmail - } - }; - - return ( - - - - ); -}; - -export default OnboardingEmailReadScreen; diff --git a/ts/screens/onboarding/OnboardingServicesPreferenceScreen.tsx b/ts/screens/onboarding/OnboardingServicesPreferenceScreen.tsx index 04aa858273a..98ef5010413 100644 --- a/ts/screens/onboarding/OnboardingServicesPreferenceScreen.tsx +++ b/ts/screens/onboarding/OnboardingServicesPreferenceScreen.tsx @@ -1,177 +1,218 @@ -import { VSpacer } from "@pagopa/io-app-design-system"; +import { + ContentWrapper, + FeatureInfo, + FooterWithButtons, + IOStyles, + IOToast, + VSpacer +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as React from "react"; +import React, { ReactElement, useCallback, useEffect, useState } from "react"; import { SafeAreaView, View } from "react-native"; -import { connect, useStore } from "react-redux"; +import { useStore } from "react-redux"; import { ServicesPreferencesModeEnum } from "../../../definitions/backend/ServicesPreferencesMode"; -import { InfoBox } from "../../components/box/InfoBox"; -import { confirmButtonProps } from "../../components/buttons/ButtonConfigurations"; -import { H5 } from "../../components/core/typography/H5"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { withLoadingSpinner } from "../../components/helpers/withLoadingSpinner"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../navigation/params/AppParamsList"; import { OnboardingParamsList } from "../../navigation/params/OnboardingParamsList"; -import { navigateToOnboardingServicePreferenceCompleteAction } from "../../store/actions/navigation"; import { servicesOptinCompleted } from "../../store/actions/onboarding"; import { profileUpsert } from "../../store/actions/profile"; -import { Dispatch } from "../../store/actions/types"; import { isServicesPreferenceModeSet, profileSelector, profileServicePreferencesModeSelector } from "../../store/reducers/profile"; -import { GlobalState } from "../../store/reducers/types"; import { getFlowType } from "../../utils/analytics"; import { emptyContextualHelp } from "../../utils/emptyContextualHelp"; import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; -import { showToast } from "../../utils/showToast"; import { trackServiceConfiguration, trackServiceConfigurationScreen } from "../profile/analytics"; import { useManualConfigBottomSheet } from "../profile/components/services/ManualConfigBottomSheet"; import ServicesContactComponent from "../profile/components/services/ServicesContactComponent"; +import { useIODispatch, useIOSelector } from "../../store/hooks"; +import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; +import { usePrevious } from "../../utils/hooks/usePrevious"; +import ROUTES from "../../navigation/routes"; export type OnboardingServicesPreferenceScreenNavigationParams = { isFirstOnboarding: boolean; }; -type Props = ReturnType & - ReturnType & - IOStackNavigationRouteProps< - OnboardingParamsList, - "ONBOARDING_SERVICES_PREFERENCE" - >; - -const OnboardingServicesPreferenceScreen = ( - props: Props -): React.ReactElement => { +type Props = IOStackNavigationRouteProps< + OnboardingParamsList, + "ONBOARDING_SERVICES_PREFERENCE" +>; + +const OnboardingServicesPreferenceScreen = (props: Props): ReactElement => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); const isFirstOnboarding = props.route.params.isFirstOnboarding; + const store = useStore(); + const profile = useIOSelector(profileSelector); + const prevProfile = usePrevious(profile); + const isLoading = pot.isUpdating(profile) || pot.isLoading(profile); + const profileServicePreferenceMode = useIOSelector( + profileServicePreferencesModeSelector + ); + const prevMode = usePrevious(profileServicePreferenceMode); + // if the user is not new and he/she hasn't a preference set, pre-set with AUTO mode - const mode = props.profileServicePreferenceMode; - const [modeSelected, setModeSelected] = React.useState< + const mode = profileServicePreferenceMode; + const [modeSelected, setModeSelected] = useState< ServicesPreferencesModeEnum | undefined >(mode); - const [prevPotProfile, setPrevPotProfile] = React.useState< - typeof props.potProfile - >(props.potProfile); - const { profileServicePreferenceMode, potProfile, onContinue } = props; + const dispatchServicesOptinCompleted = useCallback( + () => dispatch(servicesOptinCompleted()), + [dispatch] + ); + + const onServicePreferenceSelected = useCallback( + (mode: ServicesPreferencesModeEnum) => + dispatch( + profileUpsert.request({ service_preferences_settings: { mode } }) + ), + [dispatch] + ); + + const navigateToOnboardingServicePreferenceComplete = useCallback(() => { + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_SERVICES_PREFERENCE_COMPLETE + }); + }, [navigation]); + + const onContinue = useCallback( + (isFirstOnboarding: boolean) => + // if the user is not new, navigate to the thank-you screen + !isFirstOnboarding + ? navigateToOnboardingServicePreferenceComplete() + : dispatchServicesOptinCompleted(), + [ + dispatchServicesOptinCompleted, + navigateToOnboardingServicePreferenceComplete + ] + ); + + const handleOnContinue = useCallback(() => { + void trackServiceConfiguration( + profileServicePreferenceMode, + getFlowType(true, isFirstOnboarding), + store.getState() + ); + onContinue(isFirstOnboarding); + }, [isFirstOnboarding, onContinue, profileServicePreferenceMode, store]); + + const selectCurrentMode = useCallback( + (mode: ServicesPreferencesModeEnum) => { + onServicePreferenceSelected(mode); + }, + [onServicePreferenceSelected] + ); + + const { present: confirmManualConfig, manualConfigBottomSheet } = + useManualConfigBottomSheet(() => { + selectCurrentMode(ServicesPreferencesModeEnum.MANUAL); + }); + + const handleOnSelectMode = useCallback( + (mode: ServicesPreferencesModeEnum) => { + // if user's choice is 'manual', open bottom sheet to ask confirmation + if (mode === ServicesPreferencesModeEnum.MANUAL) { + confirmManualConfig(); + return; + } + selectCurrentMode(mode); + }, + [confirmManualConfig, selectCurrentMode] + ); useOnFirstRender(() => { trackServiceConfigurationScreen(getFlowType(true, isFirstOnboarding)); }); - const store = useStore(); - - React.useEffect(() => { - // when the user made a choice (the profile is right updated), continue to the next step - if (isServicesPreferenceModeSet(profileServicePreferenceMode)) { - void trackServiceConfiguration( - profileServicePreferenceMode, - getFlowType(true, isFirstOnboarding), - store.getState() - ); - onContinue(isFirstOnboarding); - return; - } + useEffect(() => { // show error toast only when the profile updating fails - // otherwise, if the profile is in error state, the toast will be shown immediately without any updates - if (!pot.isError(prevPotProfile) && pot.isError(potProfile)) { - showToast(I18n.t("global.genericError")); - } - setPrevPotProfile(potProfile); - }, [ - isFirstOnboarding, - prevPotProfile, - potProfile, - profileServicePreferenceMode, - onContinue, - store - ]); - - const handleOnContinue = () => { - if (modeSelected) { - props.onServicePreferenceSelected(modeSelected); + // otherwise, if the profile is in error state, + // the toast will be shown immediately without any updates + if (prevProfile && !pot.isError(prevProfile) && pot.isError(profile)) { + IOToast.error(I18n.t("global.genericError")); + return; } - }; - const { present: confirmManualConfig, manualConfigBottomSheet } = - useManualConfigBottomSheet(() => - props.onServicePreferenceSelected(ServicesPreferencesModeEnum.MANUAL) - ); - const handleOnSelectMode = (mode: ServicesPreferencesModeEnum) => { - // if user's choice is 'manual', open bottom sheet to ask confirmation - if (mode === ServicesPreferencesModeEnum.MANUAL) { - confirmManualConfig(); - return; + // if profile preferences are updated correctly + // the button is selected + // and the success banner is shown + if ( + prevProfile && + pot.isUpdating(prevProfile) && + pot.isSome(profile) && + profileServicePreferenceMode !== prevMode + ) { + setModeSelected(profileServicePreferenceMode); + IOToast.success( + profileServicePreferenceMode === ServicesPreferencesModeEnum.MANUAL + ? I18n.t("services.optIn.preferences.manualConfig.successAlert") + : I18n.t("services.optIn.preferences.quickConfig.successAlert") + ); } - setModeSelected(mode); - }; + }, [prevMode, prevProfile, profile, profileServicePreferenceMode]); // show a badge when the user is not new + // As explained in this comment (https://pagopa.atlassian.net/browse/IOPID-1511?focusedCommentId=126354) + // a future feature will be need this value, + // so I didn't delete it even though it is not used const showBadge = !isFirstOnboarding; return ( - + + handleOnContinue(), + accessibilityLabel: I18n.t("global.buttons.confirm"), + disabled: !isServicesPreferenceModeSet(modeSelected) + } }} /> + } + > + + + + + + + + + {manualConfigBottomSheet} - } - > - - - - -
- {I18n.t("profile.main.privacy.shareData.screen.profileSettings")} -
-
- -
- - {manualConfigBottomSheet} -
-
+
+ ); }; -const mapStateToProps = (state: GlobalState) => { - const profile = profileSelector(state); - return { - isLoading: pot.isUpdating(profile) || pot.isLoading(profile), - potProfile: profile, - profileServicePreferenceMode: profileServicePreferencesModeSelector(state) - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onContinue: (isFirstOnboarding: boolean) => - // if the user is not new, navigate to the thank-you screen - !isFirstOnboarding - ? navigateToOnboardingServicePreferenceCompleteAction() - : dispatch(servicesOptinCompleted()), - onServicePreferenceSelected: (mode: ServicesPreferencesModeEnum) => - dispatch(profileUpsert.request({ service_preferences_settings: { mode } })) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withLoadingSpinner(OnboardingServicesPreferenceScreen)); +export default OnboardingServicesPreferenceScreen; diff --git a/ts/screens/onboarding/OnboardingShareDataScreen.tsx b/ts/screens/onboarding/OnboardingShareDataScreen.tsx index feeb6b5785b..024ceedf5e0 100644 --- a/ts/screens/onboarding/OnboardingShareDataScreen.tsx +++ b/ts/screens/onboarding/OnboardingShareDataScreen.tsx @@ -71,8 +71,10 @@ const OnboardingShareDataScreen = (props: Props): React.ReactElement => { return ( diff --git a/ts/screens/onboarding/UnlockAccessScreen.tsx b/ts/screens/onboarding/UnlockAccessScreen.tsx index 6be74c5b78a..67f92c9329e 100644 --- a/ts/screens/onboarding/UnlockAccessScreen.tsx +++ b/ts/screens/onboarding/UnlockAccessScreen.tsx @@ -14,18 +14,19 @@ import { } from "@pagopa/io-app-design-system"; import { SafeAreaView } from "react-native-safe-area-context"; import { Text, View } from "react-native"; -import { useNavigation } from "@react-navigation/native"; import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; import I18n from "../../i18n"; import { useIOBottomSheetAutoresizableModal } from "../../utils/hooks/bottomSheet"; import { openWebUrl } from "../../utils/url"; import ROUTES from "../../navigation/routes"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; + type Props = { identifier: "SPID" | "CIE"; }; const UnlockAccessScreen = (props: Props) => { const { identifier } = props; - const navigation = useNavigation(); + const navigation = useIONavigation(); const ModalContent = () => ( @@ -100,7 +101,10 @@ const UnlockAccessScreen = (props: Props) => { testID: "button-link-test", label: I18n.t("authentication.unlock.loginIO"), accessibilityLabel: I18n.t("authentication.unlock.loginIO"), - onPress: () => navigation.navigate(ROUTES.AUTHENTICATION_LANDING) + onPress: () => + navigation.navigate(ROUTES.AUTHENTICATION, { + screen: ROUTES.AUTHENTICATION_LANDING + }) }} > diff --git a/ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx b/ts/screens/onboarding/__tests__/EmailInsertScreen.test.tsx similarity index 55% rename from ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx rename to ts/screens/onboarding/__tests__/EmailInsertScreen.test.tsx index 8c0ef321eac..d4e93d6b443 100644 --- a/ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx +++ b/ts/screens/onboarding/__tests__/EmailInsertScreen.test.tsx @@ -1,19 +1,18 @@ -import { fireEvent, waitFor } from "@testing-library/react-native"; +import { fireEvent } from "@testing-library/react-native"; import { createStore } from "redux"; import ROUTES from "../../../navigation/routes"; import { applicationChangeState } from "../../../store/actions/application"; import { appReducer } from "../../../store/reducers"; import I18n from "../../../i18n"; import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper"; -import CduEmailInsertScreen from "../../profile/CduEmailInsertScreen"; +import EmailInsertScreen from "../../profile/EmailInsertScreen"; -describe("CduEmailInsertScreen", async () => { +describe("EmailInsertScreen", async () => { it("the components into the page should be render correctly", () => { const component = renderComponent(); expect(component).toBeDefined(); expect(component.getByTestId("container-test")).not.toBeNull(); expect(component.getByTestId("title-test")).toBeDefined(); - expect(component.getByTestId("TextFieldInput")).toBeDefined(); expect( component.queryByText(I18n.t("global.buttons.continue")) ).toBeDefined(); @@ -28,32 +27,6 @@ describe("CduEmailInsertScreen", async () => { fireEvent.press(continueButton); } }); - - it("should show the correct error for the email insert field", async () => { - const component = renderComponent(); - const TextFieldInput = component.getByTestId("TextFieldInput"); - const continueButton = component.queryByText( - I18n.t("global.buttons.continue") - ); - expect(continueButton).toBeTruthy(); - - fireEvent.changeText(TextFieldInput, "email.email.it"); - fireEvent(TextFieldInput, "onEndEditing"); - - await waitFor(() => { - expect(continueButton).toBeDisabled(); - }); - - fireEvent.changeText(TextFieldInput, "email.email@prova.it"); - fireEvent(TextFieldInput, "onEndEditing"); - - await waitFor(() => { - expect(continueButton).not.toBeDisabled(); - if (continueButton) { - fireEvent.press(continueButton); - } - }); - }); }); const renderComponent = () => { @@ -61,7 +34,7 @@ const renderComponent = () => { const store = createStore(appReducer, globalState as any); return renderScreenWithNavigationStoreContext( - CduEmailInsertScreen, + EmailInsertScreen, ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, {}, store diff --git a/ts/screens/onboarding/__tests__/OnboardingCompletedScreen.test.tsx b/ts/screens/onboarding/__tests__/OnboardingCompletedScreen.test.tsx index 4b61cae8dca..298f33905b0 100644 --- a/ts/screens/onboarding/__tests__/OnboardingCompletedScreen.test.tsx +++ b/ts/screens/onboarding/__tests__/OnboardingCompletedScreen.test.tsx @@ -17,9 +17,7 @@ describe("Given the OnboardingCompletedScreen", () => { describe("when the user taps on the continue button", () => { it("then the completeOnboarding action is dispatched", () => { const { screen, store } = renderComponent(); - const continueButton = screen.queryByText( - I18n.t("global.buttons.continue") - ); + const continueButton = screen.queryByText(I18n.t("global.buttons.close")); expect(continueButton).toBeTruthy(); if (continueButton) { fireEvent.press(continueButton); diff --git a/ts/screens/profile/CalendarsPreferencesScreen.tsx b/ts/screens/profile/CalendarsPreferencesScreen.tsx index ce66ab23c66..61b52afd5ac 100644 --- a/ts/screens/profile/CalendarsPreferencesScreen.tsx +++ b/ts/screens/profile/CalendarsPreferencesScreen.tsx @@ -51,7 +51,9 @@ class CalendarsPreferencesScreen extends React.PureComponent { const { isLoading } = this.state; return ( ; - -type Props = IOStackNavigationRouteProps< - ProfileParamsList, - "INSERT_EMAIL_SCREEN" ->; - -const styles = StyleSheet.create({ - flex: { - flex: 1 - } -}); - -const EMPTY_EMAIL = ""; - -// TODO: update content (https://www.pivotaltracker.com/n/projects/2048617/stories/169392558) -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "email.insert.help.title", - body: "email.insert.help.content" -}; - -/** - * A screen to allow user to insert an email address. - */ -const CduEmailInsertScreen = (props: Props) => { - const viewRef = createRef(); - const { showModal } = useContext(LightModalContext); - - const dispatch = useIODispatch(); - - const profile = useIOSelector(profileSelector); - const optionEmail = useIOSelector(profileEmailSelector); - const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); - const isFirstOnboarding = useIOSelector(isProfileFirstOnBoardingSelector); - const isProfileEmailAlreadyTaken = useIOSelector( - isProfileEmailAlreadyTakenSelector - ); - - const isFirstOnBoarding = useIOSelector(isProfileFirstOnBoardingSelector); - const { isOnboarding } = props.route.params ?? {}; - - const flow = getFlowType(isOnboarding, isFirstOnBoarding); - - useOnFirstRender(() => { - if (isProfileEmailAlreadyTaken) { - trackEmailEditing(flow); - } else { - trackEmailDuplicateEditing(flow); - } - }); - - const acknowledgeOnEmailValidated = useIOSelector( - emailValidationSelector - ).acknowledgeOnEmailValidated; - - const prevUserProfile = usePrevious(profile); - - const isLoading = useMemo( - () => pot.isUpdating(profile) || pot.isLoading(profile), - [profile] - ); - - const acknowledgeEmail = useCallback( - () => dispatch(emailAcknowledged()), - [dispatch] - ); - - const updateEmail = useCallback( - (email: EmailString) => - dispatch( - profileUpsert.request({ - email - }) - ), - [dispatch] - ); - - const getEmail = (email: O.Option) => - !isProfileEmailAlreadyTaken ? email : O.some(EMPTY_EMAIL); - - const [areSameEmails, setAreSameEmails] = useState(false); - const [email, setEmail] = useState(getEmail(optionEmail)); - - useEffect(() => { - if (areSameEmails) { - trackEmailEditingError(flow); - } - }, [areSameEmails, flow]); - - /** validate email returning three possible values: - * - _true_, if email is valid. - * - _false_, if email has been already changed from the user and it is not - * valid. - * - _undefined_, if email field is empty. This state is consumed by - * LabelledItem Component and it used for style pourposes ONLY. - */ - const isValidEmail = useCallback( - () => - pipe( - email, - O.map(value => { - if ( - EMPTY_EMAIL === value || - !validator.isEmail(value) || - areSameEmails - ) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ), - [areSameEmails, email] - ); - const isContinueButtonDisabled = !isValidEmail() && !isLoading; - - const continueOnPress = () => { - Keyboard.dismiss(); - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - }; - - const renderFooterButtons = () => { - const continueButtonProps = { - disabled: isContinueButtonDisabled, - onPress: continueOnPress, - label: I18n.t("global.buttons.continue"), - accessibilityLabel: I18n.t("global.buttons.continue"), - block: true, - primary: isValidEmail() - }; - - return ( - - ); - }; - - const handleOnChangeEmailText = (value: string) => { - /** - * SCENARIOS: - * 1. first onboarding and email already taken => if the CIT writes - * the same email as the one he has to modify, he is blocked. - * 2. first onboarding and NOT email already taken => in this case, - * the CIT does not need his email to be compared with another one, - * so the areSameEmails will always be false. - * 3. Not first onboarding => if the CIT write the same email as the one - * he already has, he is blocked. - */ - if (isFirstOnBoarding) { - setAreSameEmails( - isProfileEmailAlreadyTaken - ? areStringsEqual(O.some(value), optionEmail, true) - : false - ); - } else { - setAreSameEmails(areStringsEqual(O.some(value), optionEmail, true)); - } - setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); - }; - - const handleGoBack = useCallback(() => { - // goback if the onboarding is completed - props.navigation.goBack(); - }, [props.navigation]); - - useOnFirstRender(() => { - if (!isFirstOnBoarding) { - setEmail(O.some(EMPTY_EMAIL)); - setAreSameEmails(false); - } - }); - - // If we navigate to this screen with acknowledgeOnEmailValidated set to false, - // we show the modal to remind the user to validate the email. - // This is used during the check of the email at startup. - useEffect(() => { - if ( - O.isSome(acknowledgeOnEmailValidated) && - acknowledgeOnEmailValidated.value === false - ) { - showModal( - - ); - } - }, [acknowledgeOnEmailValidated, isFirstOnboarding, showModal]); - - // eslint-disable-next-line sonarjs/cognitive-complexity - useEffect(() => { - if (prevUserProfile && pot.isUpdating(prevUserProfile)) { - if (pot.isError(profile)) { - // the user is trying to enter an email already in use - if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { - Alert.alert( - I18n.t("email.insert.alertTitle"), - I18n.t("email.insert.alertDescription"), - [ - { - text: I18n.t("email.insert.alertButton"), - style: "cancel" - } - ] - ); - } else { - showToast(I18n.t("email.edit.upsert_ko")); - } - // display a toast with error - } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { - // the email is correctly inserted - if (isEmailValidated) { - if (!isFirstOnboarding) { - handleGoBack(); - } - } else { - showModal( - - ); - } - return; - } - } - }, [ - acknowledgeEmail, - handleGoBack, - isEmailValidated, - isFirstOnboarding, - prevUserProfile, - profile, - showModal - ]); - - const showGoBack = () => { - if (isFirstOnBoarding) { - return undefined; - } else { - if (!isEmailValidated) { - return undefined; - } - return handleGoBack; - } - }; - - return ( - - - - - -

- {isFirstOnboarding - ? I18n.t("email.newinsert.title") - : I18n.t("email.edit.title")} -

- - - - {isFirstOnboarding ? ( - I18n.t("email.newinsert.subtitle") - ) : ( - <> - {I18n.t("email.edit.subtitle")} - - {` ${pipe( - optionEmail, - O.getOrElse(() => "") - )}`} - - - )} - - {isProfileEmailAlreadyTaken && isFirstOnboarding && ( - <> - - EMPTY_EMAIL) - ) - })} - /> - - )} - -
- - EMPTY_EMAIL) - ), - onChangeText: handleOnChangeEmailText - }} - testID="TextField" - /> - {areSameEmails && ( - - - - - - {I18n.t("email.newinsert.alert.description")} - - - )} - -
-
-
- {withKeyboard(renderFooterButtons())} -
-
-
- ); -}; - -export default CduEmailInsertScreen; diff --git a/ts/screens/profile/CgnLandingPlayground.tsx b/ts/screens/profile/CgnLandingPlayground.tsx index 660d9a9f939..4b191efefc6 100644 --- a/ts/screens/profile/CgnLandingPlayground.tsx +++ b/ts/screens/profile/CgnLandingPlayground.tsx @@ -1,17 +1,32 @@ -import { Content } from "native-base"; +import { + ButtonOutline, + ButtonSolid, + IOColors, + IOVisualCostants, + VSpacer +} from "@pagopa/io-app-design-system"; import * as React from "react"; -import { View, SafeAreaView, StyleSheet, TextInput } from "react-native"; -import { IOColors, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import { Label } from "../../components/core/typography/Label"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { H5 } from "../../components/core/typography/H5"; +import { + SafeAreaView, + ScrollView, + StyleSheet, + TextInput, + View +} from "react-native"; import WebviewComponent from "../../components/WebviewComponent"; +import { H5 } from "../../components/core/typography/H5"; +import { IOStyles } from "../../components/core/variables/IOStyles"; +import { useHeaderSecondLevel } from "../../hooks/useHeaderSecondLevel"; const styles = StyleSheet.create({ - textInput: { padding: 1, borderWidth: 1, height: 30, color: IOColors.black }, - contentCenter: { justifyContent: "center" }, + textInput: { + padding: 8, + borderWidth: 1, + height: 40, + color: IOColors.black, + borderRadius: 8, + borderColor: IOColors["grey-450"] + }, row: { flexDirection: "row", justifyContent: "space-between", @@ -25,63 +40,69 @@ const CgnLandingPlayground = () => { const [loadUri, setLoadUri] = React.useState("https://google.com"); const [reloadKey, setReloadKey] = React.useState(0); + useHeaderSecondLevel({ + title: "CGN Landing Playground" + }); + return ( - - - - -
{"Link alla landing"}
- - -
{"Referer"}
- -
- - - setReloadKey(r => r + 1)} - > - - - { - setLoadUri(navigationURI); + + + +
{"Link alla landing"}
+ + +
{"Referer"}
+ +
+ + + setReloadKey(r => r + 1)} + /> + { + setLoadUri(navigationURI); + }} + accessibilityLabel={"Invia"} + /> + + + + {loadUri !== "" && ( + - -
-
- - - {loadUri !== "" && ( - - )} - -
-
-
+ /> + )} +
+ +
); }; diff --git a/ts/screens/profile/DeveloperModeSection.tsx b/ts/screens/profile/DeveloperModeSection.tsx index 27f1e8bf0ca..1aa185f971c 100644 --- a/ts/screens/profile/DeveloperModeSection.tsx +++ b/ts/screens/profile/DeveloperModeSection.tsx @@ -3,20 +3,21 @@ import { ContentWrapper, Divider, H2, + IOToast, IOVisualCostants, ListItemHeader, ListItemInfoCopy, ListItemNav, ListItemSwitch, - VSpacer + VSpacer, + useIOTheme, + useIOThemeContext } from "@pagopa/io-app-design-system"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { useNavigation } from "@react-navigation/native"; import I18n from "i18n-js"; import * as React from "react"; import { ComponentProps } from "react"; import { Alert, FlatList, ListRenderItemInfo } from "react-native"; -import { IOToast } from "../../components/Toast"; import { AlertModal } from "../../components/ui/AlertModal"; import { LightModalContext } from "../../components/ui/LightModal"; import { isPlaygroundsEnabled } from "../../config"; @@ -24,11 +25,14 @@ import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selec import { lollipopPublicKeySelector } from "../../features/lollipop/store/reducers/lollipop"; import { toThumbprint } from "../../features/lollipop/utils/crypto"; import { walletAddCoBadgeStart } from "../../features/wallet/onboarding/cobadge/store/actions"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import ROUTES from "../../navigation/routes"; import { sessionExpired } from "../../store/actions/authentication"; import { setDebugModeEnabled } from "../../store/actions/debug"; import { preferencesIdPayTestSetEnabled, + preferencesItWalletTestSetEnabled, + preferencesNewWalletSectionSetEnabled, preferencesPagoPaTestEnvironmentSetEnabled, preferencesPnTestEnvironmentSetEnabled } from "../../store/actions/persistedPreferences"; @@ -42,12 +46,15 @@ import { isDebugModeEnabledSelector } from "../../store/reducers/debug"; import { notificationsInstallationSelector } from "../../store/reducers/notifications/installation"; import { isIdPayTestEnabledSelector, + isItWalletTestEnabledSelector, + isNewWalletSectionEnabledSelector, isPagoPATestEnabledSelector, isPnTestEnabledSelector } from "../../store/reducers/persistedPreferences"; import { clipboardSetStringWithFeedback } from "../../utils/clipboard"; import { getDeviceId } from "../../utils/device"; import { isDevEnv } from "../../utils/environment"; + import DSEnableSwitch from "./components/DSEnableSwitch"; type PlaygroundsNavListItem = { @@ -66,6 +73,10 @@ type DevDataCopyListItem = { "label" | "testID" | "onPress" >; +type DevActionButton = { + condition: boolean; +} & Pick, "color" | "label" | "onPress">; + const DeveloperActionsSection = () => { const dispatch = useIODispatch(); @@ -91,78 +102,83 @@ const DeveloperActionsSection = () => { ); }; - return ( - - + const dumpAsyncStorage = () => { + /* eslint-disable no-console */ + console.log("[DUMP START]"); + AsyncStorage.getAllKeys() + .then(keys => { + console.log(`\tAvailable keys: ${keys.join(", ")}`); + return Promise.all( + keys.map(key => + AsyncStorage.getItem(key).then(value => { + console.log(`\tValue for ${key}\n\t\t`, value); + }) + ) + ); + }) + .then(() => console.log("[DUMP END]")) + .catch(e => console.error(e)); + /* eslint-enable no-console */ + }; - - - - {isDevEnv && ( - <> - - dispatch(sessionExpired())} - accessibilityLabel={I18n.t("profile.main.forgetCurrentSession")} - /> - - - )} - {isDevEnv && ( - <> - - { - void AsyncStorage.clear(); - }} - accessibilityLabel={I18n.t("profile.main.clearAsyncStorage")} - /> - - - )} - {isDevEnv && ( - <> - - { - /* eslint-disable no-console */ - console.log("[DUMP START]"); - AsyncStorage.getAllKeys() - .then(keys => { - console.log(`\tAvailable keys: ${keys.join(", ")}`); - return Promise.all( - keys.map(key => - AsyncStorage.getItem(key).then(value => { - console.log(`\tValue for ${key}\n\t\t`, value); - }) - ) - ); - }) - .then(() => console.log("[DUMP END]")) - .catch(e => console.error(e)); - /* eslint-enable no-console */ - }} - accessibilityLabel={I18n.t("profile.main.dumpAsyncStorage")} - /> - - - )} - + const devActionButtons: ReadonlyArray = [ + { + condition: true, + label: I18n.t("profile.main.cache.clear"), + onPress: handleClearCachePress + }, + { + condition: isDevEnv, + label: I18n.t("profile.main.forgetCurrentSession"), + onPress: () => dispatch(sessionExpired()) + }, + { + condition: isDevEnv, + label: I18n.t("profile.main.clearAsyncStorage"), + onPress: () => { + void AsyncStorage.clear(); + } + }, + { + condition: isDevEnv, + color: "primary", + label: I18n.t("profile.main.dumpAsyncStorage"), + onPress: dumpAsyncStorage + } + ]; + + // Don't render the separator, even if the item is null + const filteredDevActionButtons = devActionButtons.filter( + item => item.condition !== false + ); + + const renderDevActionButton = ({ + item: { color = "danger", label, onPress } + }: ListRenderItemInfo) => ( + + ); + + return ( + } + scrollEnabled={false} + keyExtractor={(item: DevActionButton, index: number) => + `${item.label}-${index}` + } + contentContainerStyle={{ + paddingHorizontal: IOVisualCostants.appMarginDefault + }} + data={filteredDevActionButtons} + renderItem={renderDevActionButton} + ItemSeparatorComponent={() => } + ListFooterComponent={() => } + /> ); }; @@ -225,7 +241,7 @@ const DeveloperDataSection = () => { ]; // Don't render the separator, even if the item is null - const filtereddevDataCopyListItems = devDataCopyListItems.filter( + const filteredDevDataCopyListItems = devDataCopyListItems.filter( item => item.condition !== false ); @@ -259,7 +275,7 @@ const DeveloperDataSection = () => { contentContainerStyle={{ paddingHorizontal: IOVisualCostants.appMarginDefault }} - data={filtereddevDataCopyListItems} + data={filteredDevDataCopyListItems} renderItem={renderDevDataCopyItem} ItemSeparatorComponent={() => } /> @@ -267,7 +283,21 @@ const DeveloperDataSection = () => { }; const DesignSystemSection = () => { - const navigation = useNavigation(); + const navigation = useIONavigation(); + const { themeType, setTheme } = useIOThemeContext(); + const dispatch = useIODispatch(); + + const isNewWalletSectionEnabled = useIOSelector( + isNewWalletSectionEnabledSelector + ); + + const onNewWalletSectionToggle = (enabled: boolean) => { + dispatch( + preferencesNewWalletSectionSetEnabled({ + isNewWalletSectionEnabled: enabled + }) + ); + }; return ( @@ -284,13 +314,28 @@ const DesignSystemSection = () => { /> + + + setTheme(themeType === "dark" ? "light" : "dark") + } + /> + + ); }; const PlaygroundsSection = () => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const isIdPayTestEnabled = useIOSelector(isIdPayTestEnabledSelector); + const isItWalletTestEnabled = useIOSelector(isItWalletTestEnabledSelector); const playgroundsNavListItems: ReadonlyArray = [ { value: "Lollipop", @@ -337,12 +382,16 @@ const PlaygroundsSection = () => { }) }, { - // New Wallet - value: I18n.t("profile.main.walletPlayground.titleSection"), + value: "Payments", onPress: () => navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { screen: ROUTES.WALLET_PLAYGROUND }) + }, + { + condition: isItWalletTestEnabled, + value: "IT Wallet", + onPress: () => undefined } ]; @@ -395,6 +444,7 @@ const DeveloperTestEnvironmentSection = ({ const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); const isPnTestEnabled = useIOSelector(isPnTestEnabledSelector); const isIdPayTestEnabled = useIOSelector(isIdPayTestEnabledSelector); + const isItWalletTestEnabled = useIOSelector(isItWalletTestEnabledSelector); const onAddTestCard = () => { if (!isPagoPATestEnabled) { Alert.alert( @@ -458,6 +508,12 @@ const DeveloperTestEnvironmentSection = ({ dispatch(preferencesIdPayTestSetEnabled({ isIdPayTestEnabled: enabled })); handleShowModal(); }; + + const onItWalletTestToggle = (enabled: boolean) => { + dispatch( + preferencesItWalletTestSetEnabled({ isItWalletTestEnabled: enabled }) + ); + }; return ( + ); }; @@ -498,6 +560,8 @@ const DeveloperModeSection = () => { const dispatch = useIODispatch(); const isDebugModeEnabled = useIOSelector(isDebugModeEnabledSelector); + const theme = useIOTheme(); + const handleShowModal = () => { showModal( { <> -

{I18n.t("profile.main.developersSectionHeader")}

+

+ {I18n.t("profile.main.developersSectionHeader")} +

{/* Enable/Disable Developer Mode */} diff --git a/ts/screens/profile/DownloadProfileDataScreen.tsx b/ts/screens/profile/DownloadProfileDataScreen.tsx index 859caf9b5c6..f29080a1e05 100644 --- a/ts/screens/profile/DownloadProfileDataScreen.tsx +++ b/ts/screens/profile/DownloadProfileDataScreen.tsx @@ -1,14 +1,18 @@ +import { + BlockButtonProps, + ContentWrapper, + IOToast, + FooterWithButtons +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { useNavigation } from "@react-navigation/native"; import React, { useEffect, useState } from "react"; -import { Alert, SafeAreaView, StyleSheet, View } from "react-native"; +import { Alert, SafeAreaView } from "react-native"; import { UserDataProcessingChoiceEnum } from "../../../definitions/backend/UserDataProcessingChoice"; -import { IOStyles } from "../../components/core/variables/IOStyles"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import ScreenContent from "../../components/screens/ScreenContent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; -import Markdown from "../../components/ui/Markdown"; +import { IOStyles } from "../../components/core/variables/IOStyles"; +import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; +import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../i18n"; import { resetUserDataProcessingRequest, @@ -16,16 +20,7 @@ import { } from "../../store/actions/userDataProcessing"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { userDataProcessingSelector } from "../../store/reducers/userDataProcessing"; -import themeVariables from "../../theme/variables"; import { usePrevious } from "../../utils/hooks/usePrevious"; -import { showToast } from "../../utils/showToast"; - -const styles = StyleSheet.create({ - container: { - paddingLeft: themeVariables.contentPadding, - paddingRight: themeVariables.contentPadding - } -}); /** * A screen to explain how profile data export works. @@ -49,7 +44,7 @@ const DownloadProfileDataScreen = () => { pot.isSome(userDataProcessing.DOWNLOAD) ) { if (pot.isError(userDataProcessing.DOWNLOAD)) { - showToast(I18n.t("profile.main.privacy.exportData.error")); + IOToast.error(I18n.t("profile.main.privacy.exportData.error")); return; } navigation.goBack(); @@ -85,37 +80,42 @@ const DownloadProfileDataScreen = () => { ); }; + const requestDataButtonProps: BlockButtonProps = { + type: "Solid", + buttonProps: { + color: "primary", + label: I18n.t("profile.main.privacy.exportData.cta"), + accessibilityLabel: I18n.t("profile.main.privacy.exportData.cta"), + onPress: handleDownloadPress + } + }; + return ( - - + + ) + } + > + - - - setIsMarkdownLoaded(true)}> - {I18n.t("profile.main.privacy.exportData.info.body")} - - - - {isMarkdownLoaded && ( - - )} + + setIsMarkdownLoaded(true)}> + {I18n.t("profile.main.privacy.exportData.info.body")} + + - - + +
); }; diff --git a/ts/screens/profile/EmailInsertScreen.tsx b/ts/screens/profile/EmailInsertScreen.tsx index b2ab2d2e56f..12e8c3ef64e 100644 --- a/ts/screens/profile/EmailInsertScreen.tsx +++ b/ts/screens/profile/EmailInsertScreen.tsx @@ -4,56 +4,75 @@ */ import * as pot from "@pagopa/ts-commons/lib/pot"; import { EmailString } from "@pagopa/ts-commons/lib/strings"; -import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import { Content, Form } from "native-base"; -import * as React from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Alert, Keyboard, SafeAreaView, StyleSheet, View } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { H1 } from "../../components/core/typography/H1"; -import { LabelledItem } from "../../components/LabelledItem"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import validator from "validator"; +import { + Alert, + Keyboard, + KeyboardAvoidingView, + Platform, + View, + StyleSheet +} from "react-native"; +import { + VSpacer, + H1, + TextInputValidation, + ContentWrapper, + ButtonSolid, + IOToast +} from "@pagopa/io-app-design-system"; +import { Route, useFocusEffect, useRoute } from "@react-navigation/native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { ScrollView } from "react-native-gesture-handler"; +import themeVariables from "../../theme/variables"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; -import BaseScreenComponent, { - ContextualHelpPropsMarkdown -} from "../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; +import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; -import { emailInsert } from "../../store/actions/onboarding"; -import { profileLoadRequest, profileUpsert } from "../../store/actions/profile"; +import { profileUpsert } from "../../store/actions/profile"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { + isProfileEmailAlreadyTakenSelector, isProfileEmailValidatedSelector, + isProfileFirstOnBoardingSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; import { usePrevious } from "../../utils/hooks/usePrevious"; -import { withKeyboard } from "../../utils/keyboard"; import { areStringsEqual } from "../../utils/options"; -import { showToast } from "../../utils/showToast"; import { Body } from "../../components/core/typography/Body"; -import { IOStyles } from "../../components/core/variables/IOStyles"; import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; -import { trackEmailEditing } from "../analytics/emailAnalytics"; +import { + trackEmailDuplicateEditing, + trackEmailEditing, + trackEmailEditingError, + trackSendValidationEmail +} from "../analytics/emailAnalytics"; import { getFlowType } from "../../utils/analytics"; +import { emailValidationSelector } from "../../store/reducers/emailValidation"; +import { useHeaderSecondLevel } from "../../hooks/useHeaderSecondLevel"; +import { trackTosUserExit } from "../authentication/analytics"; +import { abortOnboarding } from "../../store/actions/onboarding"; +import ROUTES from "../../navigation/routes"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { setAccessibilityFocus } from "../../utils/accessibility"; -type Props = IOStackNavigationRouteProps< - ProfileParamsList, - "INSERT_EMAIL_SCREEN" ->; - -const styles = StyleSheet.create({ - flex: { - flex: 1 - } -}); +export type EmailInsertScreenNavigationParams = Readonly<{ + isOnboarding: boolean; + isFciEditEmailFlow?: boolean; + isEditingPreviouslyInsertedEmailMode?: boolean; +}>; const EMPTY_EMAIL = ""; -// TODO: update content (https://www.pivotaltracker.com/n/projects/2048617/stories/169392558) const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "email.insert.help.title", body: "email.insert.help.content" @@ -62,21 +81,69 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { /** * A screen to allow user to insert an email address. */ -const EmailInsertScreen = (props: Props) => { +const EmailInsertScreen = () => { + const { + isOnboarding, + isFciEditEmailFlow, + isEditingPreviouslyInsertedEmailMode + } = + useRoute< + Route< + "ONBOARDING_INSERT_EMAIL_SCREEN" | "INSERT_EMAIL_SCREEN", + EmailInsertScreenNavigationParams + > + >().params; + const navigation = useIONavigation(); + const dispatch = useIODispatch(); const profile = useIOSelector(profileSelector); const optionEmail = useIOSelector(profileEmailSelector); const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); - const isLoading = useMemo( - () => pot.isUpdating(profile) || pot.isLoading(profile), - [profile] + const isFirstOnboarding = useIOSelector(isProfileFirstOnBoardingSelector); + const isProfileEmailAlreadyTaken = useIOSelector( + isProfileEmailAlreadyTakenSelector ); + const [errorMessage, setErrorMessage] = useState(""); + const flow = getFlowType(isOnboarding, isFirstOnboarding); + const accessibilityFirstFocuseViewRef = useRef(null); + // This reference is used to prevent the refresh visual glitch + // caused by the polling stop in the email validation screen. + const canShowLoadingSpinner = useRef(true); + + useFocusEffect(() => setAccessibilityFocus(accessibilityFirstFocuseViewRef)); useOnFirstRender(() => { - trackEmailEditing(getFlowType(false)); + if (isProfileEmailAlreadyTaken) { + trackEmailEditing(flow); + if (isFirstOnboarding) { + IOToast.info( + I18n.t("email.newinsert.alert.title", { + email: pipe( + optionEmail, + O.getOrElse(() => EMPTY_EMAIL) + ) + }) + ); + } + } else { + trackEmailDuplicateEditing(flow); + } }); + const acknowledgeOnEmailValidated = useIOSelector( + emailValidationSelector + ).acknowledgeOnEmailValidated; + + const prevUserProfile = usePrevious(profile); + + const isLoading = useMemo( + () => + (pot.isUpdating(profile) || pot.isLoading(profile)) && + canShowLoadingSpinner.current, + [profile] + ); + const updateEmail = useCallback( (email: EmailString) => dispatch( @@ -86,186 +153,339 @@ const EmailInsertScreen = (props: Props) => { ), [dispatch] ); - const acknowledgeEmailInsert = useCallback( - () => dispatch(emailInsert()), - [dispatch] - ); - const reloadProfile = useCallback( - () => dispatch(profileLoadRequest()), + + const dispatchAbortOnboarding = useCallback( + () => dispatch(abortOnboarding()), [dispatch] ); - const [email, setEmail] = useState(optionEmail); + const getEmail = (email: O.Option) => + !isProfileEmailAlreadyTaken ? email : O.some(EMPTY_EMAIL); + + const [areSameEmails, setAreSameEmails] = useState(false); + const [email, setEmail] = useState(getEmail(optionEmail)); - /** validate email returning three possible values: + useEffect(() => { + if (areSameEmails) { + trackEmailEditingError(flow); + } + }, [areSameEmails, flow]); + + const sameEmailsErrorRender = useCallback(() => { + if (isProfileEmailAlreadyTaken && isFirstOnboarding) { + setErrorMessage(I18n.t("email.newinsert.alert.description1")); + return; + } + if (isOnboarding) { + setErrorMessage(I18n.t("email.newinsert.alert.description2")); + return; + } + if (!isOnboarding && !isFirstOnboarding) { + setErrorMessage(I18n.t("email.newinsert.alert.description3")); + return; + } + setErrorMessage(I18n.t("email.newinsert.alert.description1")); + }, [isFirstOnboarding, isOnboarding, isProfileEmailAlreadyTaken]); + + /** validate email returning two possible values: * - _true_, if email is valid. * - _false_, if email has been already changed from the user and it is not * valid. - * - _undefined_, if email field is empty. This state is consumed by - * LabelledItem Component and it used for style pourposes ONLY. */ - const isValidEmail = () => - pipe( - email, - O.map(value => { - if (EMPTY_EMAIL === value) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ); + const isValidEmail = useCallback( + () => + pipe( + email, + O.fold( + () => { + setErrorMessage(I18n.t("email.newinsert.alert.invalidemail")); + return false; + }, + value => { + if (!validator.isEmail(value)) { + setErrorMessage(I18n.t("email.newinsert.alert.invalidemail")); + return false; + } + if (areSameEmails) { + sameEmailsErrorRender(); + return false; + } + return true; + } + ) + ), + [areSameEmails, email, sameEmailsErrorRender] + ); const continueOnPress = () => { Keyboard.dismiss(); + // eslint-disable-next-line functional/immutable-data + canShowLoadingSpinner.current = true; if (isValidEmail()) { - // The profile is reloaded to check if the user email - // has been updated within another session - reloadProfile(); + pipe( + email, + O.map(e => { + updateEmail(e as EmailString); + }) + ); + if (isFirstOnboarding) { + trackSendValidationEmail(flow); + } } }; - const renderFooterButtons = () => { - const continueButtonProps = { - disabled: isValidEmail() !== true && !isLoading, - onPress: continueOnPress, - title: I18n.t("global.buttons.continue"), - block: true, - primary: isValidEmail() - }; - - return ( - - ); - }; - const handleOnChangeEmailText = (value: string) => { + /** + * SCENARIOS: + * 1. first onboarding and email already taken => if the CIT writes + * the same email as the one he has to modify, he is blocked. + * 2. first onboarding and NOT email already taken => in this case, + * the CIT does not need his email to be compared with another one, + * so the areSameEmails will always be false. + * 3. Not first onboarding => if the CIT write the same email as the one + * he already has, he is blocked. + */ + // If we are editing the email previously inserted + // we don't want to show the error message. + if (!isEditingPreviouslyInsertedEmailMode) { + if (isFirstOnboarding) { + setAreSameEmails( + isProfileEmailAlreadyTaken + ? areStringsEqual(O.some(value), optionEmail, true) + : false + ); + } else { + setAreSameEmails(areStringsEqual(O.some(value), optionEmail, true)); + } + } setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); }; const handleGoBack = useCallback(() => { - // goback if the onboarding is completed - props.navigation.goBack(); - }, [props.navigation]); - - useEffect(() => { - setEmail(O.some(EMPTY_EMAIL)); - }, []); + // click on goback icon + // if the flow is onboarding, a warning is displayed at the click + if (isFirstOnboarding) { + Alert.alert( + I18n.t("onboarding.alert.title"), + I18n.t("onboarding.alert.description"), + [ + { + text: I18n.t("global.buttons.cancel"), + style: "cancel" + }, + { + text: I18n.t("global.buttons.exit"), + style: "default", + onPress: () => { + trackTosUserExit(getFlowType(true, isFirstOnboarding)); + dispatchAbortOnboarding(); + } + } + ] + ); + // if the flow isn't first onboarding/onboarding + // the button allows you to return to the previous step + } else { + navigation.goBack(); + } + }, [dispatchAbortOnboarding, isFirstOnboarding, navigation]); - const prevUserProfile = usePrevious(profile); + useOnFirstRender(() => { + if (!isFirstOnboarding) { + setEmail(O.some(EMPTY_EMAIL)); + setAreSameEmails(false); + } + }); - const prevOptionEmail = usePrevious(optionEmail); + const userNavigateToEmailValidationScreen = + O.isSome(acknowledgeOnEmailValidated) && + acknowledgeOnEmailValidated.value === false && + isOnboarding; + // If we navigate to this screen with acknowledgeOnEmailValidated set to false, + // let the user navigate the email validation screen useEffect(() => { - if (prevUserProfile) { - const isPrevCurrentSameState = prevUserProfile.kind === profile.kind; - // do nothing if prev profile is in the same state of the current - if (isPrevCurrentSameState) { - return; - } + if (userNavigateToEmailValidationScreen) { + // eslint-disable-next-line functional/immutable-data + canShowLoadingSpinner.current = false; + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_EMAIL_VERIFICATION_SCREEN, + params: { + isOnboarding, + sendEmailAtFirstRender: isOnboarding + } + }); } - }, [prevUserProfile, profile]); + }, [isOnboarding, navigation, userNavigateToEmailValidationScreen]); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { + // the user is trying to enter an email already in use + if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { + Alert.alert( + I18n.t("email.insert.alertTitle"), + I18n.t("email.insert.alertDescription"), + [ + { + text: I18n.t("email.insert.alertButton"), + style: "cancel" + } + ] + ); + } else { + IOToast.error(I18n.t("email.edit.upsert_ko")); + } // display a toast with error - showToast(I18n.t("email.edit.upsert_ko"), "danger"); - } else if (pot.isSome(profile)) { - // user is inserting his email from onboarding phase - // he comes from checkAcknowledgedEmailSaga if onboarding is not finished yet - // and he has not an email - // go back (to the EmailReadScreen) - handleGoBack(); - return; + } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { + // the email is correctly inserted + + // if the email is entered and when the 'confirm' button + // is clicked the session has expired, when the session + // is refreshed the profile is updated and the email is + // validated because we are still using the old email. + // In order to prevent the user navigating to the email + // validation screen we use this control to allow the user + // to remain in this screen + if (isEmailValidated) { + if (!isFirstOnboarding) { + return; + } + } else { + // eslint-disable-next-line functional/no-let + let sendEmailAtFirstRender = false; + // the IO BE orchestrator already send an email + // if the previous profile email is different from the current one. + if (pot.isSome(prevUserProfile)) { + // So we need to check if the email is not changed + // to send the email validation process programmatically. + sendEmailAtFirstRender = + profile.value.email === prevUserProfile.value.email; + } + // eslint-disable-next-line functional/immutable-data + canShowLoadingSpinner.current = false; + if (isOnboarding) { + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_EMAIL_VERIFICATION_SCREEN, + params: { + isOnboarding, + sendEmailAtFirstRender: isOnboarding + } + }); + } else { + navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.EMAIL_VERIFICATION_SCREEN, + params: { + isOnboarding: false, + sendEmailAtFirstRender, + isFciEditEmailFlow + } + }); + } + } } } }, [ - acknowledgeEmailInsert, handleGoBack, - prevOptionEmail, + isEmailValidated, + isFciEditEmailFlow, + isFirstOnboarding, + isOnboarding, + navigation, prevUserProfile, profile ]); - useEffect(() => { - // When the profile reload is completed, check if the email is changed since the last reload - if ( - prevUserProfile && - pot.isLoading(prevUserProfile) && - !pot.isLoading(profile) - ) { - // Check both if the email has been changed within another session and - // if the inserted email match with the email stored into the user profile - const isTheSameEmail = areStringsEqual(optionEmail, email, true); - if (!isTheSameEmail) { - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - } else { - Alert.alert(I18n.t("email.insert.alert")); - } - } - }, [email, prevUserProfile, profile, optionEmail, updateEmail]); + useHeaderSecondLevel({ + title: "", + supportRequest: true, + contextualHelpMarkdown, + goBack: handleGoBack, + canGoBack: isEmailValidated || isFirstOnboarding + }); return ( - - - - -

- {I18n.t("email.edit.title")} + + + +

+ {isFirstOnboarding + ? I18n.t("email.newinsert.title") + : I18n.t("email.edit.title")}

- - - - {isEmailValidated - ? I18n.t("email.edit.validated") - : I18n.t("email.edit.subtitle")} - - {` ${pipe( - optionEmail, - O.getOrElse(() => "") - )}`} - - - -
- EMPTY_EMAIL) - ), - onChangeText: handleOnChangeEmailText - }} - /> -
- - {withKeyboard(renderFooterButtons())} - - + + + {isFirstOnboarding ? ( + I18n.t("email.newinsert.subtitle") + ) : ( + <> + {I18n.t("email.edit.subtitle")} + + {` ${pipe( + optionEmail, + O.getOrElse(() => "") + )}`} + + + )} + + + EMPTY_EMAIL) + )} + onChangeText={handleOnChangeEmailText} + /> +
+
+ + + + + + + ); }; export default EmailInsertScreen; + +const styles = StyleSheet.create({ + safeArea: { + flexGrow: 1 + }, + scrollViewContentContainer: { + flexGrow: 1 + } +}); diff --git a/ts/screens/profile/EmailReadScreen.tsx b/ts/screens/profile/EmailReadScreen.tsx deleted file mode 100644 index f40a5e6b135..00000000000 --- a/ts/screens/profile/EmailReadScreen.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * A screen to display the email address used by IO - * The _isFromProfileSection_ navigation parameter let the screen being adapted - * if: - * - it is displayed during the user onboarding - * - it is displayed after the onboarding (navigation from the profile section) - */ -import { useNavigation } from "@react-navigation/native"; -import * as React from "react"; -import EmailReadComponent from "../../components/EmailReadComponent"; -import { SingleButton } from "../../components/ui/BlockButtons"; -import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; -import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; -import ROUTES from "../../navigation/routes"; - -type Props = IOStackNavigationRouteProps< - ProfileParamsList, - "READ_EMAIL_SCREEN" ->; - -const EmailReadScreen = (props: Props) => { - useValidatedEmailModal(); - const navigation = useNavigation(); - - const handleGoBack = () => { - props.navigation.goBack(); - }; - - const footerProps: SingleButton = { - type: "SingleButton", - leftButton: { - bordered: true, - title: I18n.t("email.edit.cta"), - onPress: () => - navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.INSERT_EMAIL_SCREEN - }) - } - }; - - return ( - - ); -}; - -export default EmailReadScreen; diff --git a/ts/screens/profile/EmailValidationSendEmailScreen.tsx b/ts/screens/profile/EmailValidationSendEmailScreen.tsx new file mode 100644 index 00000000000..26266fc7839 --- /dev/null +++ b/ts/screens/profile/EmailValidationSendEmailScreen.tsx @@ -0,0 +1,354 @@ +/** + * A component to remind the user to validate his email + */ +import { Millisecond } from "@pagopa/ts-commons/lib/units"; +import { pipe } from "fp-ts/lib/function"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import React, { useRef, useCallback, useEffect, useState } from "react"; +import { Platform, ScrollView, StyleSheet, View } from "react-native"; +import { + IOPictogramSizeScale, + Pictogram, + VSpacer, + Body, + IOStyles, + H3, + IOVisualCostants, + ButtonOutline, + ButtonSolid, + ButtonLink, + IOToast +} from "@pagopa/io-app-design-system"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { Route, useFocusEffect, useRoute } from "@react-navigation/native"; +import _ from "lodash"; +import I18n from "../../i18n"; + +import { + acknowledgeOnEmailValidation, + emailValidationPollingStart, + emailValidationPollingStop, + setEmailCheckAtStartupFailure, + startEmailValidation +} from "../../store/actions/profile"; +import { + isEmailValidatedSelector, + isProfileFirstOnBoardingSelector, + profileEmailSelector +} from "../../store/reducers/profile"; +import { useIODispatch, useIOSelector } from "../../store/hooks"; +import { emailValidationSelector } from "../../store/reducers/emailValidation"; +import { emailAcknowledged } from "../../store/actions/onboarding"; +import { getFlowType } from "../../utils/analytics"; +import { + trackEmailValidation, + trackEmailValidationSuccess, + trackEmailValidationSuccessConfirmed, + trackResendValidationEmail +} from "../analytics/emailAnalytics"; +import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; +import { usePrevious } from "../../utils/hooks/usePrevious"; +import { CountdownProvider } from "../../components/countdown/CountdownProvider"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { setAccessibilityFocus } from "../../utils/accessibility"; +import { FCI_ROUTES } from "../../features/fci/navigation/routes"; +import ROUTES from "../../navigation/routes"; +import Countdown from "./components/CountdownComponent"; + +const emailSentTimeout = 60000 as Millisecond; // 60 seconds +const countdownIntervalDuration = 1000 as Millisecond; // 1 second + +const EMPTY_EMAIL = ""; +const VALIDATION_ILLUSTRATION_WIDTH: IOPictogramSizeScale = 120; + +export type SendEmailValidationScreenProp = { + isOnboarding?: boolean; + sendEmailAtFirstRender?: boolean; + isFciEditEmailFlow?: boolean; + isEditingEmailMode?: boolean; +}; + +const EmailValidationSendEmailScreen = () => { + const props = + useRoute< + Route< + "ONBOARDING_EMAIL_VERIFICATION_SCREEN" | "EMAIL_VERIFICATION_SCREEN", + SendEmailValidationScreenProp + > + >().params; + const { isOnboarding, sendEmailAtFirstRender, isFciEditEmailFlow } = props; + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const optionEmail = useIOSelector(profileEmailSelector, _.isEqual); + const isEmailValidated = useIOSelector(isEmailValidatedSelector); + const emailValidation = useIOSelector(emailValidationSelector, _.isEqual); + const prevEmailValidation = usePrevious(emailValidation); + const isFirstOnBoarding = useIOSelector(isProfileFirstOnBoardingSelector); + const flow = getFlowType(!!isOnboarding, isFirstOnBoarding); + const [showCountdown, setShowCountdown] = useState(true); + const email = pipe( + optionEmail, + O.getOrElse(() => EMPTY_EMAIL) + ); + const accessibilityFirstFocuseViewRef = useRef(null); + + const sendEmailValidation = useCallback( + () => dispatch(startEmailValidation.request()), + [dispatch] + ); + + const acknowledgeEmail = useCallback( + () => dispatch(emailAcknowledged()), + [dispatch] + ); + + const startPollingSaga = useCallback( + () => dispatch(emailValidationPollingStart()), + [dispatch] + ); + const stopPollingSaga = useCallback( + () => dispatch(emailValidationPollingStop()), + [dispatch] + ); + + const dispatchAcknowledgeOnEmailValidation = useCallback( + (maybeAcknowledged: O.Option) => + dispatch(acknowledgeOnEmailValidation(maybeAcknowledged)), + [dispatch] + ); + + const dispatchSetEmailCheckAtStartupFailure = useCallback( + (maybeFailed: O.Option) => + dispatch(setEmailCheckAtStartupFailure(maybeFailed)), + [dispatch] + ); + + useFocusEffect(() => setAccessibilityFocus(accessibilityFirstFocuseViewRef)); + + useOnFirstRender(() => { + // polling starts every time the user land on this screen + startPollingSaga(); + // if the verification email was never sent, we send it + if (sendEmailAtFirstRender) { + sendEmailValidation(); + } + }); + + const handleContinue = useCallback(() => { + if (isEmailValidated) { + trackEmailValidationSuccessConfirmed(flow); + if (isOnboarding || isFirstOnBoarding) { + acknowledgeEmail(); + if ( + O.isSome(emailValidation.emailCheckAtStartupFailed) && + emailValidation.emailCheckAtStartupFailed.value + ) { + dispatchAcknowledgeOnEmailValidation(O.none); + dispatchSetEmailCheckAtStartupFailure(O.none); + // if the user is in the onboarding flow and the email is correctly validated, + // the email validation flow is finished + } + } else { + if (isFciEditEmailFlow) { + navigation.navigate(FCI_ROUTES.MAIN, { + screen: FCI_ROUTES.USER_DATA_SHARE + }); + } else { + navigation.popToTop(); + } + } + } + }, [ + acknowledgeEmail, + dispatchAcknowledgeOnEmailValidation, + dispatchSetEmailCheckAtStartupFailure, + emailValidation.emailCheckAtStartupFailed, + flow, + isEmailValidated, + isFciEditEmailFlow, + isFirstOnBoarding, + isOnboarding, + navigation + ]); + + const handleResendEmail = useCallback(() => { + trackResendValidationEmail(flow); + sendEmailValidation(); + }, [flow, sendEmailValidation]); + + const navigateBackToInsertEmail = useCallback(() => { + dispatchAcknowledgeOnEmailValidation(O.none); + if (isOnboarding) { + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { + isOnboarding, + isFciEditEmailFlow, + isEditingPreviouslyInsertedEmailMode: true + } + }); + } else { + navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.INSERT_EMAIL_SCREEN, + params: { + isOnboarding: false, + isFciEditEmailFlow, + isEditingPreviouslyInsertedEmailMode: true + } + }); + } + }, [ + dispatchAcknowledgeOnEmailValidation, + isFciEditEmailFlow, + isOnboarding, + navigation + ]); + + useEffect(() => { + if ( + prevEmailValidation && + pot.isLoading(prevEmailValidation.sendEmailValidationRequest) + ) { + // send validation email KO + if (pot.isError(emailValidation.sendEmailValidationRequest)) { + IOToast.error(I18n.t("global.actions.retry")); + setShowCountdown(false); + } + // send validation email OK + if (pot.isSome(emailValidation.sendEmailValidationRequest)) { + IOToast.success(I18n.t("email.newvalidate.toast")); + setShowCountdown(true); + } + } + }, [emailValidation.sendEmailValidationRequest, prevEmailValidation]); + + useEffect(() => { + if (isEmailValidated) { + setShowCountdown(false); + // if the user has validated the email the polling can stop + trackEmailValidationSuccess(flow); + } else { + trackEmailValidation(flow); + } + + return () => { + // if the user change screen the polling can stop + if (!isEmailValidated) { + stopPollingSaga(); + } + }; + }, [flow, isEmailValidated, stopPollingSaga]); + + return ( + + + + + + + +

+ {I18n.t( + isEmailValidated + ? "email.newvalidemail.title" + : "email.newvalidate.title" + )} +

+
+ + + + {I18n.t( + isEmailValidated + ? "email.newvalidemail.subtitle" + : "email.newvalidate.subtitle" + )}{" "} + {email} + + + + {!isEmailValidated && ( + + + + + )} + + { + setShowCountdown(false); + }} + visible={showCountdown && !isEmailValidated} + /> + + {isEmailValidated ? ( + + + + ) : ( + !showCountdown && ( + + + + ) + )} +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + marginHorizontal: IOVisualCostants.appMarginDefault + }, + wrapper: { + flex: 1, + alignItems: "stretch", + justifyContent: "center", + alignContent: "center" + }, + wrapper_android: { + flexGrow: 1, + justifyContent: "center" + } +}); + +export default EmailValidationSendEmailScreen; diff --git a/ts/screens/profile/LanguagesPreferencesScreen.tsx b/ts/screens/profile/LanguagesPreferencesScreen.tsx index 1bda954db7f..6e4c5c6c07e 100644 --- a/ts/screens/profile/LanguagesPreferencesScreen.tsx +++ b/ts/screens/profile/LanguagesPreferencesScreen.tsx @@ -8,12 +8,14 @@ import { Alert, SafeAreaView, View } from "react-native"; import { connect } from "react-redux"; import { Locales, TranslationKeys } from "../../../locales/locales"; import SectionStatusComponent from "../../components/SectionStatus"; -import { withLightModalContext } from "../../components/helpers/withLightModalContext"; import { withLoadingSpinner } from "../../components/helpers/withLoadingSpinner"; import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import ListItemComponent from "../../components/screens/ListItemComponent"; import { AlertModal } from "../../components/ui/AlertModal"; -import { LightModalContextInterface } from "../../components/ui/LightModal"; +import { + LightModalContext, + LightModalContextInterface +} from "../../components/ui/LightModal"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n, { availableTranslations } from "../../i18n"; import { preferredLanguageSaveSuccess } from "../../store/actions/persistedPreferences"; @@ -28,12 +30,13 @@ import { } from "../../utils/locale"; import { showToast } from "../../utils/showToast"; -type Props = LightModalContextInterface & - ReturnType & +type Props = ReturnType & ReturnType; type State = { isLoading: boolean; selectedLocale?: Locales }; +type LanguagesPreferencesScreenProps = Props & LightModalContextInterface; + const iconSize: IOIconSizeScale = 12; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { @@ -44,8 +47,11 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { /** * Allows the user to select one of the available Languages as preferred */ -class LanguagesPreferencesScreen extends React.PureComponent { - constructor(props: Props) { +class LanguagesPreferencesScreen extends React.PureComponent< + LanguagesPreferencesScreenProps, + State +> { + constructor(props: LanguagesPreferencesScreenProps) { super(props); this.state = { isLoading: false }; } @@ -122,7 +128,9 @@ class LanguagesPreferencesScreen extends React.PureComponent { public render() { const ContainerComponent = withLoadingSpinner(() => ( ({ ) }); +const LanguagesPreferencesScreenFC = (props: Props) => { + const { ...modalContext } = React.useContext(LightModalContext); + return ; +}; + export default connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(LanguagesPreferencesScreen)); +)(LanguagesPreferencesScreenFC); diff --git a/ts/screens/profile/NotificationsPreferencesScreen.tsx b/ts/screens/profile/NotificationsPreferencesScreen.tsx index b65b1110aa8..b1cbc875396 100644 --- a/ts/screens/profile/NotificationsPreferencesScreen.tsx +++ b/ts/screens/profile/NotificationsPreferencesScreen.tsx @@ -60,7 +60,9 @@ export const NotificationsPreferencesScreen = () => { return ( ; - -type Props = OwnProps & - ReturnType & - ReturnType & - ReduxProps & - LightModalContextInterface; +type Props = ReturnType & + ReturnType; type PreferencesNavListItem = { value: string; @@ -70,6 +68,9 @@ type PreferencesNavListItem = { "description" | "testID" | "onPress" >; +type PreferencesScreenProps = LightModalContextInterface & + Props & { navigation: IOStackNavigationProp }; + /** * Translates the primary languages of the provided locales. * @@ -105,8 +106,8 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { body: "profile.preferences.contextualHelpContent" }; -class PreferencesScreen extends React.Component { - constructor(props: Props) { +class PreferencesScreen extends React.Component { + constructor(props: PreferencesScreenProps) { super(props); } @@ -243,7 +244,9 @@ class PreferencesScreen extends React.Component { return ( ({ navigateToLanguagePreferenceScreen: () => navigateToLanguagePreferenceScreen() }); +const PreferencesScreenFC = (props: Props) => { + const { ...modalContext } = React.useContext(LightModalContext); + const navigation = useIONavigation(); + return ( + + ); +}; + export default connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(PreferencesScreen)); +)(PreferencesScreenFC); diff --git a/ts/screens/profile/PrivacyMainScreen.tsx b/ts/screens/profile/PrivacyMainScreen.tsx index f1a6aaff75f..1ca07463e8d 100644 --- a/ts/screens/profile/PrivacyMainScreen.tsx +++ b/ts/screens/profile/PrivacyMainScreen.tsx @@ -262,7 +262,9 @@ const PrivacyMainScreen = ({ navigation }: Props) => { return ( diff --git a/ts/screens/profile/ProfileAboutApp.tsx b/ts/screens/profile/ProfileAboutApp.tsx index fea8fc79f62..7fb55c47919 100644 --- a/ts/screens/profile/ProfileAboutApp.tsx +++ b/ts/screens/profile/ProfileAboutApp.tsx @@ -1,19 +1,21 @@ import { ContentWrapper, VSpacer } from "@pagopa/io-app-design-system"; import React from "react"; -import Markdown from "../../components/ui/Markdown"; +import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../i18n"; const ProfileAboutApp = () => ( - + {I18n.t("profile.main.appInfo.contextualHelpContent")} - + diff --git a/ts/screens/profile/ProfileDataScreen.tsx b/ts/screens/profile/ProfileDataScreen.tsx index 600bebcdc18..a1024eb7b3c 100644 --- a/ts/screens/profile/ProfileDataScreen.tsx +++ b/ts/screens/profile/ProfileDataScreen.tsx @@ -3,16 +3,10 @@ import { pipe } from "fp-ts/lib/function"; import { List } from "native-base"; import * as React from "react"; import { connect } from "react-redux"; -import { Dispatch } from "redux"; import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import ListItemComponent from "../../components/screens/ListItemComponent"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; -import { isEmailUniquenessValidationEnabledSelector } from "../../features/fastLogin/store/selectors"; import I18n from "../../i18n"; -import { - navigateToEmailInsertScreen, - navigateToEmailReadScreen -} from "../../store/actions/navigation"; import { hasProfileEmailSelector, isProfileEmailValidatedSelector, @@ -20,9 +14,10 @@ import { profileNameSurnameSelector } from "../../store/reducers/profile"; import { GlobalState } from "../../store/reducers/types"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import ROUTES from "../../navigation/routes"; -type Props = ReturnType & - ReturnType; +type Props = ReturnType; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "profile.preferences.contextualHelpTitle", @@ -32,26 +27,30 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { const ProfileDataScreen: React.FC = ({ profileEmail, isEmailValidated, - isEmailUniquenessValidationEnabled, - navigateToEmailReadScreen, - navigateToEmailInsertScreen, hasProfileEmail, nameSurname }): React.ReactElement => { + const navigation = useIONavigation(); + + const navigateToInsertEmailScreen = React.useCallback(() => { + navigation.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.INSERT_EMAIL_SCREEN, + params: { + isOnboarding: false + } + }); + }, [navigation]); + const onPressEmail = () => { if (hasProfileEmail) { - if (isEmailUniquenessValidationEnabled) { - navigateToEmailInsertScreen(); - } else { - navigateToEmailReadScreen(); - } - } else { - navigateToEmailInsertScreen(); + navigateToInsertEmailScreen(); } }; return ( = ({ ); }; -const mapDispatchToProps = (_: Dispatch) => ({ - navigateToEmailReadScreen: () => navigateToEmailReadScreen(), - navigateToEmailInsertScreen: () => navigateToEmailInsertScreen() -}); - const mapStateToProps = (state: GlobalState) => ({ profileEmail: profileEmailSelector(state), isEmailValidated: isProfileEmailValidatedSelector(state), hasProfileEmail: hasProfileEmailSelector(state), - nameSurname: profileNameSurnameSelector(state), - isEmailUniquenessValidationEnabled: - isEmailUniquenessValidationEnabledSelector(state) + nameSurname: profileNameSurnameSelector(state) }); -export default connect(mapStateToProps, mapDispatchToProps)(ProfileDataScreen); +export default connect(mapStateToProps)(ProfileDataScreen); diff --git a/ts/screens/profile/ProfileMainScreen.tsx b/ts/screens/profile/ProfileMainScreen.tsx index 4165dfa186e..5e5ccde9195 100644 --- a/ts/screens/profile/ProfileMainScreen.tsx +++ b/ts/screens/profile/ProfileMainScreen.tsx @@ -3,8 +3,10 @@ import { Divider, IOColors, IOVisualCostants, + IOToast, ListItemNav, - VSpacer + VSpacer, + useIOTheme } from "@pagopa/io-app-design-system"; import { Millisecond } from "@pagopa/ts-commons/lib/units"; import * as React from "react"; @@ -20,9 +22,7 @@ import { import { connect } from "react-redux"; import AppVersion from "../../components/AppVersion"; import FiscalCodeComponent from "../../components/FiscalCodeComponent"; -import { IOToast } from "../../components/Toast"; import TouchableDefaultOpacity from "../../components/TouchableDefaultOpacity"; -import { IOStyles } from "../../components/core/variables/IOStyles"; import { withLightModalContext } from "../../components/helpers/withLightModalContext"; import { TabBarItemPressType, @@ -197,47 +197,6 @@ class ProfileMainScreen extends React.PureComponent { } ]; - const renderProfileNavItem = ({ - item: { value, description, onPress, testID, hideChevron } - }: ListRenderItemInfo) => ( - - ); - - const screenContent = () => ( - - - - `${item.value}-${index}` - } - contentContainerStyle={{ - paddingHorizontal: IOVisualCostants.appMarginDefault - }} - data={profileNavListItems} - renderItem={renderProfileNavItem} - ItemSeparatorComponent={() => } - /> - - - - - - {/* Developer Section */} - {(isDebugModeEnabled || isDevEnv) && } - - {/* End Page */} - - - ); - /* The dimensions of the screen that will be used to hide the white background when inertial scrolling is turned on. */ @@ -288,11 +247,67 @@ class ProfileMainScreen extends React.PureComponent { contextualHelpMarkdown={contextualHelpMarkdown} faqCategories={["profile"]} > - {screenContent()} + ); } } +type ScreenContentProps = { + isDebugModeEnabled: boolean; + onTapAppVersion: () => void; + profileNavListItems: ReadonlyArray; +}; + +const ScreenContent = ({ + profileNavListItems, + onTapAppVersion, + isDebugModeEnabled +}: ScreenContentProps) => { + const theme = useIOTheme(); + const renderProfileNavItem = ({ + item: { value, description, onPress, testID, hideChevron } + }: ListRenderItemInfo) => ( + + ); + return ( + + + + `${item.value}-${index}` + } + contentContainerStyle={{ + paddingHorizontal: IOVisualCostants.appMarginDefault + }} + data={profileNavListItems} + renderItem={renderProfileNavItem} + ItemSeparatorComponent={() => } + /> + + + + + + {/* Developer Section */} + {(isDebugModeEnabled || isDevEnv) && } + + {/* End Page */} + + + ); +}; const mapStateToProps = (state: GlobalState) => ({ isDebugModeEnabled: isDebugModeEnabledSelector(state) diff --git a/ts/screens/profile/RemoveAccountDetailsScreen.tsx b/ts/screens/profile/RemoveAccountDetailsScreen.tsx index 091d97de597..d576f10f3c3 100644 --- a/ts/screens/profile/RemoveAccountDetailsScreen.tsx +++ b/ts/screens/profile/RemoveAccountDetailsScreen.tsx @@ -1,39 +1,38 @@ +import { + BlockButtonProps, + ContentWrapper, + FooterWithButtons +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { Content } from "native-base"; +import { StackActions } from "@react-navigation/native"; import * as React from "react"; -import { View, Alert, SafeAreaView } from "react-native"; +import { Alert, SafeAreaView } from "react-native"; import { connect } from "react-redux"; import { Dispatch } from "redux"; -import { StackActions } from "@react-navigation/native"; +import { LabelledItem } from "../../components/LabelledItem"; +import { LoadingErrorComponent } from "../../components/LoadingErrorComponent"; import { RadioButtonList, RadioItem } from "../../components/core/selection/RadioButtonList"; -import { H1 } from "../../components/core/typography/H1"; -import { H4 } from "../../components/core/typography/H4"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import { LabelledItem } from "../../components/LabelledItem"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; +import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import { shufflePinPadOnPayment } from "../../config"; -import { LoadingErrorComponent } from "../../components/LoadingErrorComponent"; import { isCgnEnrolledSelector } from "../../features/bonus/cgn/store/reducers/details"; import I18n from "../../i18n"; import NavigationService from "../../navigation/NavigationService"; import { identificationRequest } from "../../store/actions/identification"; import { navigateToWalletHome } from "../../store/actions/navigation"; import { - removeAccountMotivation, RemoveAccountMotivationEnum, - RemoveAccountMotivationPayload + RemoveAccountMotivationPayload, + removeAccountMotivation } from "../../store/actions/profile"; -import { ReduxProps } from "../../store/actions/types"; import { GlobalState } from "../../store/reducers/types"; import { userDataProcessingSelector } from "../../store/reducers/userDataProcessing"; import { withKeyboard } from "../../utils/keyboard"; -type Props = ReduxProps & - ReturnType & +type Props = ReturnType & ReturnType; const getMotivationItems = (): ReadonlyArray< @@ -117,20 +116,33 @@ const RemoveAccountDetails: React.FunctionComponent = (props: Props) => { props.requestIdentification({ reason: selectedMotivation }); } }; - const continueButtonProps = { - block: true, - primary: true, - onPress: handleContinuePress, - title: I18n.t("profile.main.privacy.removeAccount.info.cta") + + const continueButtonProps: BlockButtonProps = { + type: "Solid", + buttonProps: { + color: "primary", + label: I18n.t("profile.main.privacy.removeAccount.info.cta"), + accessibilityLabel: I18n.t("profile.main.privacy.removeAccount.info.cta"), + onPress: handleContinuePress + } }; const loadingCaption = I18n.t( "profile.main.privacy.removeAccount.success.title" ); return ( - , + true + )} > {props.isLoading || props.isError ? ( = (props: Props) => { /> ) : ( - -

{I18n.t("profile.main.privacy.removeAccount.title")}

-

- {I18n.t("profile.main.privacy.removeAccount.details.body")} -

- - - head={I18n.t( - "profile.main.privacy.removeAccount.details.question" + + + head={I18n.t( + "profile.main.privacy.removeAccount.details.question" + )} + key="delete_reason" + items={getMotivationItems()} + selectedItem={selectedMotivation} + onPress={setSelectedMotivation} + /> + {selectedMotivation === RemoveAccountMotivationEnum.OTHERS && ( + - {selectedMotivation === RemoveAccountMotivationEnum.OTHERS && ( - - )} - -
- {withKeyboard( - , - true - )} + )} +
)} -
+
); }; diff --git a/ts/screens/profile/RemoveAccountInfoScreen.tsx b/ts/screens/profile/RemoveAccountInfoScreen.tsx index 8b49a343c21..518efa44414 100644 --- a/ts/screens/profile/RemoveAccountInfoScreen.tsx +++ b/ts/screens/profile/RemoveAccountInfoScreen.tsx @@ -1,9 +1,12 @@ -import { Body, ContentWrapper } from "@pagopa/io-app-design-system"; +import { + BlockButtonProps, + Body, + ContentWrapper, + FooterWithButtons +} from "@pagopa/io-app-design-system"; import * as React from "react"; -import { SafeAreaView } from "react-native"; import { connect } from "react-redux"; import { Dispatch } from "redux"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../i18n"; import { navigateToRemoveAccountDetailScreen } from "../../store/actions/navigation"; @@ -16,28 +19,30 @@ type Props = ReturnType; * Here user can ask to delete his account */ const RemoveAccountInfo: React.FunctionComponent = props => { - const continueButtonProps = { - block: true, - primary: true, - onPress: () => { - props.loadBonus(); - props.navigateToRemoveAccountDetail(); - }, - title: I18n.t("profile.main.privacy.removeAccount.info.cta") + const continueButtonProps: BlockButtonProps = { + type: "Solid", + buttonProps: { + color: "primary", + label: I18n.t("profile.main.privacy.removeAccount.info.cta"), + accessibilityLabel: I18n.t("profile.main.privacy.removeAccount.info.cta"), + onPress: () => { + props.loadBonus(); + props.navigateToRemoveAccountDetail(); + } + } }; - const footerComponent = ( - - - - ); return ( + } > diff --git a/ts/screens/profile/RemoveAccountSuccessScreen.tsx b/ts/screens/profile/RemoveAccountSuccessScreen.tsx index d5ded0b2871..78acfb8522f 100644 --- a/ts/screens/profile/RemoveAccountSuccessScreen.tsx +++ b/ts/screens/profile/RemoveAccountSuccessScreen.tsx @@ -1,67 +1,101 @@ -import { Content } from "native-base"; +import { + BlockButtonProps, + Body, + ContentWrapper, + FooterWithButtons, + H2, + IOVisualCostants, + VSpacer +} from "@pagopa/io-app-design-system"; import * as React from "react"; -import { Image, SafeAreaView, StyleSheet } from "react-native"; +import { useState } from "react"; +import { Image, Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { connect } from "react-redux"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; +import expiredIcon from "../../../img/wallet/errors/payment-expired-icon.png"; +import { useHardwareBackButton } from "../../hooks/useHardwareBackButton"; import I18n from "../../i18n"; -import { H4 } from "../../components/core/typography/H4"; -import { H2 } from "../../components/core/typography/H2"; import { AppParamsList, IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { Dispatch } from "../../store/actions/types"; import { logoutRequest } from "../../store/actions/authentication"; -import expiredIcon from "../../../img/wallet/errors/payment-expired-icon.png"; -import { useHardwareBackButton } from "../../hooks/useHardwareBackButton"; +import { Dispatch } from "../../store/actions/types"; type Props = IOStackNavigationRouteProps & ReturnType; -const styles = StyleSheet.create({ - content: { - flex: 1, - flexDirection: "column", - justifyContent: "center", - alignItems: "center" - } -}); + /** * A screen to explain how the account removal works. * Here user can ask to delete his account */ const RemoveAccountSuccess: React.FunctionComponent = props => { + const [footerHeight, setFooterHeight] = useState(0); + const insets = useSafeAreaInsets(); // do nothing useHardwareBackButton(() => true); - const continueButtonProps = { - block: true, - bordered: true, - primary: true, - onPress: props.logout, - title: I18n.t("profile.main.privacy.removeAccount.success.cta") + const continueButtonProps: BlockButtonProps = { + type: "Outline", + buttonProps: { + color: "primary", + label: I18n.t("profile.main.privacy.removeAccount.success.cta"), + accessibilityLabel: I18n.t( + "profile.main.privacy.removeAccount.success.cta" + ), + onPress: props.logout + } }; const footerComponent = ( - + ); return ( - - - - - -

{I18n.t("profile.main.privacy.removeAccount.success.title")}

-

- {I18n.t("profile.main.privacy.removeAccount.success.body")} -

-
- {footerComponent} -
-
+ + {/* This extra View is mandatory when you have a fixed + bottom component to get a consistent behavior + across platforms */} + + + + + + +

+ {I18n.t("profile.main.privacy.removeAccount.success.title")} +

+ + + {I18n.t("profile.main.privacy.removeAccount.success.body")} + +
+
+
+
+ {footerComponent} +
); }; diff --git a/ts/screens/profile/SecurityScreen.tsx b/ts/screens/profile/SecurityScreen.tsx index 26bf13f62e5..9dd37d0df3e 100644 --- a/ts/screens/profile/SecurityScreen.tsx +++ b/ts/screens/profile/SecurityScreen.tsx @@ -1,7 +1,7 @@ import { Divider, ListItemNav } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; import { List } from "native-base"; import React, { useCallback, useEffect, useState } from "react"; +import { useNavigation } from "@react-navigation/native"; import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import ListItemComponent from "../../components/screens/ListItemComponent"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; @@ -29,6 +29,11 @@ import { trackBiometricActivationAccepted, trackBiometricActivationDeclined } from "../onboarding/biometric&securityChecks/analytics"; +import { + IOStackNavigationProp, + useIONavigation +} from "../../navigation/params/AppParamsList"; +import { IdPayCodeParamsList } from "../../features/idpay/code/navigation/params"; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "profile.preferences.contextualHelpTitle", @@ -38,24 +43,36 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { const SecurityScreen = (): React.ReactElement => { const dispatch = useIODispatch(); const isFingerprintEnabled = useIOSelector(isFingerprintEnabledSelector); - const navigation = useNavigation(); + const navigation = + useNavigation>(); + const commonNavigation = useIONavigation(); const isScreenReaderEnabled = useScreenReaderEnabled(); const [isFingerprintAvailable, setIsFingerprintAvailable] = useState(false); const isIdPayEnabled = useIOSelector(isIdPayEnabledSelector); const isIdPayCodeOnboarded = useIOSelector(isIdPayCodeOnboardedSelector); const idPayCodeHandler = () => { - navigation.navigate(IdPayCodeRoutes.IDPAY_CODE_MAIN, { - screen: isIdPayCodeOnboarded - ? IdPayCodeRoutes.IDPAY_CODE_RENEW - : IdPayCodeRoutes.IDPAY_CODE_ONBOARDING - }); + navigation.navigate( + IdPayCodeRoutes.IDPAY_CODE_MAIN, + isIdPayCodeOnboarded + ? { + screen: IdPayCodeRoutes.IDPAY_CODE_RENEW + } + : { + screen: IdPayCodeRoutes.IDPAY_CODE_ONBOARDING, + params: { + initiativeId: undefined + } + } + ); }; const requestIdentificationAndResetPin = useCallback(() => { const onSuccess = () => { void mixpanelTrack("UPDATE_PIN"); - navigation.navigate(ROUTES.PIN_SCREEN); + commonNavigation.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.PIN_SCREEN + }); }; dispatch( @@ -70,7 +87,7 @@ const SecurityScreen = (): React.ReactElement => { shufflePinPadOnPayment ) ); - }, [dispatch, navigation]); + }, [commonNavigation, dispatch]); const setFingerprintPreference = useCallback( (fingerprintPreference: boolean) => @@ -122,7 +139,9 @@ const SecurityScreen = (): React.ReactElement => { return ( & - ReturnType; - /** * Display the current profile services preference mode (auto or manual) * User can update his/her mode * @param props * @constructor */ -const ServicesPreferenceScreen = (props: Props): React.ReactElement => { - const { potProfile, state } = props; +const ServicesPreferenceScreen = (): ReactElement => { + const store = useStore(); + const state = store.getState(); + const dispatch = useIODispatch(); + const profile = useIOSelector(profileSelector); + const prevProfile = usePrevious(profile); + const isLoading = pot.isUpdating(profile) || pot.isLoading(profile); + const profileServicePreferenceMode = useIOSelector( + profileServicePreferencesModeSelector + ); + const prevMode = usePrevious(profileServicePreferenceMode); + + const dispatchServicePreferencesSetting = useCallback( + (mode: ServicesPreferencesModeEnum) => + dispatch( + profileUpsert.request({ service_preferences_settings: { mode } }) + ), + [dispatch] + ); + + const onServicePreferenceSelected = useCallback( + (mode: ServicesPreferencesModeEnum, state: GlobalState) => { + void trackServiceConfiguration(mode, getFlowType(false, false), state); + dispatchServicePreferencesSetting(mode); + }, + [dispatchServicePreferencesSetting] + ); + const { present: confirmManualConfig, manualConfigBottomSheet } = useManualConfigBottomSheet(() => - props.onServicePreferenceSelected( - ServicesPreferencesModeEnum.MANUAL, - state - ) + onServicePreferenceSelected(ServicesPreferencesModeEnum.MANUAL, state) ); useOnFirstRender(() => { trackServiceConfigurationScreen(getFlowType(false, false)); }); - const [prevPotProfile, setPrevPotProfile] = React.useState< - typeof props.potProfile - >(props.potProfile); - React.useEffect(() => { + useEffect(() => { // show error toast only when the profile updating fails - // otherwise, if the profile is in error state, the toast will be shown immediately without any updates - if (!pot.isError(prevPotProfile) && pot.isError(potProfile)) { - showToast(I18n.t("global.genericError")); - } - - setPrevPotProfile(potProfile); - }, [potProfile, prevPotProfile]); - - const handleOnSelectMode = (mode: ServicesPreferencesModeEnum) => { - // if user's choice is 'manual', open bottom sheet to ask confirmation - if (mode === ServicesPreferencesModeEnum.MANUAL) { - confirmManualConfig(); + // otherwise, if the profile is in error state, + // the toast will be shown immediately without any updates + if (prevProfile && !pot.isError(prevProfile) && pot.isError(profile)) { + IOToast.error(I18n.t("global.genericError")); return; } - props.onServicePreferenceSelected(mode, state); - }; + // if profile preferences are updated correctly + // the button is selected + // and the success banner is shown + if ( + prevProfile && + pot.isUpdating(prevProfile) && + pot.isSome(profile) && + prevMode !== profileServicePreferenceMode + ) { + IOToast.success( + profileServicePreferenceMode === ServicesPreferencesModeEnum.MANUAL + ? I18n.t("services.optIn.preferences.manualConfig.successAlert") + : I18n.t("services.optIn.preferences.quickConfig.successAlert") + ); + } + }, [profile, prevProfile, profileServicePreferenceMode, prevMode]); - return ( - - - - - - {manualConfigBottomSheet} - + const handleOnSelectMode = useCallback( + (mode: ServicesPreferencesModeEnum) => { + // if user's choice is 'manual', open bottom sheet to ask confirmation + if (mode === ServicesPreferencesModeEnum.MANUAL) { + confirmManualConfig(); + return; + } + onServicePreferenceSelected(mode, state); + }, + [confirmManualConfig, onServicePreferenceSelected, state] ); -}; -const mapStateToProps = (state: GlobalState) => { - const profile = profileSelector(state); - return { - isLoading: pot.isUpdating(profile) || pot.isLoading(profile), - potProfile: profile, - profileServicePreferenceMode: profileServicePreferencesModeSelector(state), - state - }; + return ( + + + + + + + {manualConfigBottomSheet} + + + ); }; -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onServicePreferenceSelected: ( - mode: ServicesPreferencesModeEnum, - state: GlobalState - ) => { - void trackServiceConfiguration(mode, getFlowType(false, false), state); - dispatch(profileUpsert.request({ service_preferences_settings: { mode } })); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withLoadingSpinner(ServicesPreferenceScreen)); +export default ServicesPreferenceScreen; diff --git a/ts/screens/profile/ShareDataScreen.tsx b/ts/screens/profile/ShareDataScreen.tsx index 1c611526cde..87cd751c096 100644 --- a/ts/screens/profile/ShareDataScreen.tsx +++ b/ts/screens/profile/ShareDataScreen.tsx @@ -1,12 +1,12 @@ import { BlockButtonProps, + IOToast, FooterWithButtons } from "@pagopa/io-app-design-system"; import * as React from "react"; import { SafeAreaView, View } from "react-native"; import { connect } from "react-redux"; import { Dispatch } from "redux"; -import { IOToast } from "../../components/Toast"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { RNavScreenWithLargeHeader } from "../../components/ui/RNavScreenWithLargeHeader"; import I18n from "../../i18n"; @@ -77,8 +77,10 @@ const ShareDataScreen = (props: Props): React.ReactElement => { return ( diff --git a/ts/screens/profile/WebPlayground.tsx b/ts/screens/profile/WebPlayground.tsx index 23fa72b394c..cf6cdb08f92 100644 --- a/ts/screens/profile/WebPlayground.tsx +++ b/ts/screens/profile/WebPlayground.tsx @@ -1,27 +1,46 @@ -import { Content } from "native-base"; -import URLParse from "url-parse"; +import { + ButtonOutline, + ButtonSolid, + Divider, + HSpacer, + IOColors, + IOStyles, + IOVisualCostants, + IconButtonContained, + ListItemSwitch, + VSpacer, + IOToast +} from "@pagopa/io-app-design-system"; +import CookieManager, { Cookie } from "@react-native-cookies/cookies"; import * as React from "react"; -import { View, SafeAreaView, StyleSheet, TextInput } from "react-native"; +import { + SafeAreaView, + ScrollView, + StyleSheet, + TextInput, + View +} from "react-native"; import { connect } from "react-redux"; -import CookieManager, { Cookie } from "@react-native-cookies/cookies"; -import { Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; -import { Label } from "../../components/core/typography/Label"; -import BaseScreenComponent from "../../components/screens/BaseScreenComponent"; -import Switch from "../../components/ui/Switch"; -import { Monospace } from "../../components/core/typography/Monospace"; +import URLParse from "url-parse"; +import { LabelledItem } from "../../components/LabelledItem"; import RegionServiceWebView from "../../components/RegionServiceWebView"; -import { Dispatch } from "../../store/actions/types"; +import { Monospace } from "../../components/core/typography/Monospace"; +import { useHeaderSecondLevel } from "../../hooks/useHeaderSecondLevel"; import { navigateBack } from "../../store/actions/navigation"; -import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; -import { LabelledItem } from "../../components/LabelledItem"; -import { showToast } from "../../utils/showToast"; +import { Dispatch } from "../../store/actions/types"; type Props = ReturnType; const styles = StyleSheet.create({ flex: { flex: 1 }, - textInput: { flex: 1, padding: 1, borderWidth: 1, height: 30 }, - contentCenter: { justifyContent: "center" }, + textInput: { + flex: 1, + padding: 8, + borderWidth: 1, + borderRadius: 8, + borderColor: IOColors["grey-450"], + height: 40 + }, row: { flexDirection: "row", justifyContent: "space-between", @@ -39,9 +58,13 @@ const WebPlayground: React.FunctionComponent = (props: Props) => { const [saveCookie, setSaveCookie] = React.useState(false); const [reloadKey, setReloadKey] = React.useState(0); + useHeaderSecondLevel({ + title: "MyPortal Web" + }); + const setCookieOnDomain = () => { if (loadUri === "") { - showToast("Missing domain"); + IOToast.info("Missing domain"); return; } const url = new URLParse(loadUri, true); @@ -55,15 +78,15 @@ const WebPlayground: React.FunctionComponent = (props: Props) => { CookieManager.set(url.origin, cookie, true) .then(_ => { - showToast("cookie correctly set", "success"); + IOToast.success("cookie correctly set"); }) - .catch(_ => showToast("Unable to set Cookie")); + .catch(_ => IOToast.error("Unable to set Cookie")); }; const clearCookies = () => { CookieManager.clearAll(true) - .then(() => showToast("Cookies cleared", "success")) - .catch(_ => showToast("Unable to remove Cookies")); + .then(() => IOToast.success("Cookies cleared")) + .catch(_ => IOToast.error("Unable to remove Cookies")); }; const handleUriInput = (text: string) => { @@ -71,88 +94,91 @@ const WebPlayground: React.FunctionComponent = (props: Props) => { }; return ( - - - - - - - setLoadUri(navigationURI)} - > - - - - - - setReloadKey(r => r + 1)} - > - - - - - - - - - - - - - - - - - - - {saveCookie && ( - <> - - - - setCookieOnDomain()} - > - - - - )} - {showDebug && {webMessage}} - - - - - + + + + + + setLoadUri(navigationURI)} + icon="arrowRight" + accessibilityLabel={"Imposta la pagina web"} + /> + + + + setReloadKey(r => r + 1)} + icon="reload" + label="Reload" + accessibilityLabel={"Reload"} + /> + + + + + + + + + {saveCookie && ( + <> + + + + setCookieOnDomain()} + label="Save" + accessibilityLabel={"Save"} + /> + + + )} + {showDebug && {webMessage}} + + + + ); }; diff --git a/ts/screens/profile/__test__/ProfileDataScreen.test.tsx b/ts/screens/profile/__test__/ProfileDataScreen.test.tsx index 1e5079258f6..0cb5d60ac31 100644 --- a/ts/screens/profile/__test__/ProfileDataScreen.test.tsx +++ b/ts/screens/profile/__test__/ProfileDataScreen.test.tsx @@ -1,8 +1,8 @@ -import { fireEvent } from "@testing-library/react-native"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import React from "react"; import configureMockStore from "redux-mock-store"; +import { fireEvent } from "@testing-library/react-native"; import I18n from "../../../i18n"; import ROUTES from "../../../navigation/routes"; import { applicationChangeState } from "../../../store/actions/application"; diff --git a/ts/screens/profile/analytics/index.ts b/ts/screens/profile/analytics/index.ts index 3103dc1e336..ec450dc03ae 100644 --- a/ts/screens/profile/analytics/index.ts +++ b/ts/screens/profile/analytics/index.ts @@ -9,7 +9,20 @@ import { FlowType, buildEventProperties } from "../../../utils/analytics"; export async function trackProfileLoadSuccess(state: GlobalState) { await updateMixpanelSuperProperties(state); await updateMixpanelProfileProperties(state); - await mixpanelTrack(profileLoadSuccess.toString()); + mixpanelTrack(profileLoadSuccess.toString()); +} + +export function trackIdpAuthenticationSuccessScreen(idpId: string | undefined) { + void mixpanelTrack("IDENTIFICATION_CONFIRM_SCREEN", { + idp: idpId, + ...buildEventProperties("UX", "screen_view") + }); +} +export function trackIngressScreen() { + void mixpanelTrack( + "INITIALIZATION_LOADING", + buildEventProperties("UX", "screen_view") + ); } export function trackTosScreen(flow: FlowType) { @@ -74,7 +87,7 @@ export async function trackTosAccepted( property: "TOS_ACCEPTED_VERSION", value: acceptedTosVersion }); - await mixpanelTrack( + mixpanelTrack( "TOS_ACCEPTED", buildEventProperties( "UX", @@ -139,7 +152,7 @@ export async function trackServiceConfiguration( property: "SERVICE_CONFIGURATION", value: mode }); - await mixpanelTrack( + mixpanelTrack( "SERVICE_PREFERENCE_CONFIGURATION", buildEventProperties( "UX", @@ -194,7 +207,7 @@ export async function trackNotificationPreferenceConfiguration( property: "NOTIFICATION_CONFIGURATION", value: configuration }); - await mixpanelTrack( + mixpanelTrack( "NOTIFICATION_PREFERENCE_CONFIGURATION", buildEventProperties( "UX", diff --git a/ts/screens/profile/components/CountdownComponent.tsx b/ts/screens/profile/components/CountdownComponent.tsx new file mode 100644 index 00000000000..cca5dcef7fb --- /dev/null +++ b/ts/screens/profile/components/CountdownComponent.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { View } from "react-native"; +import { Body, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import { useCountdown } from "../../../components/countdown/CountdownProvider"; +import I18n from "../../../i18n"; +import { ProgressIndicator } from "../../../components/ui/ProgressIndicator"; + +type CountdownProps = { + visible: boolean; + onContdownCompleted?: () => void; +}; + +const Countdown = (props: CountdownProps) => { + const { visible } = props; + const { timerCount, resetTimer, startTimer, isRunning } = useCountdown(); + + if (timerCount === 0 && props.onContdownCompleted) { + props.onContdownCompleted(); + } + + if (!visible) { + if (resetTimer) { + resetTimer(); + } + + return null; + } else if (startTimer && isRunning && !isRunning()) { + startTimer(); + } + + if (visible) { + return ( + + + + + + {I18n.t("email.newvalidate.countdowntext")}{" "} + + {timerCount}s + + + + ); + } + return null; +}; +export default Countdown; diff --git a/ts/screens/profile/components/ShareDataComponent.tsx b/ts/screens/profile/components/ShareDataComponent.tsx index 7c6c4719c0c..61f3975bb51 100644 --- a/ts/screens/profile/components/ShareDataComponent.tsx +++ b/ts/screens/profile/components/ShareDataComponent.tsx @@ -4,7 +4,7 @@ import { InfoBox } from "../../../components/box/InfoBox"; import { Body } from "../../../components/core/typography/Body"; import { Label } from "../../../components/core/typography/Label"; import { Link } from "../../../components/core/typography/Link"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import { privacyUrl } from "../../../config"; import I18n from "../../../i18n"; import { ioSuppliersUrl } from "../../../urls"; @@ -18,7 +18,7 @@ type MarkdownProps = { const MarkdownBody = (props: MarkdownProps): React.ReactElement => ( <> - {props.body} + {props.body} ); diff --git a/ts/screens/profile/components/services/ManualConfigBottomSheet.tsx b/ts/screens/profile/components/services/ManualConfigBottomSheet.tsx index 0ce64301235..7d6d8ffc0e5 100644 --- a/ts/screens/profile/components/services/ManualConfigBottomSheet.tsx +++ b/ts/screens/profile/components/services/ManualConfigBottomSheet.tsx @@ -1,20 +1,17 @@ import * as React from "react"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; -import { - cancelButtonProps, - errorButtonProps -} from "../../../../components/buttons/ButtonConfigurations"; -import Markdown from "../../../../components/ui/Markdown"; +import { VSpacer, FooterWithButtons } from "@pagopa/io-app-design-system"; +import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; import I18n from "../../../../i18n"; -import { useLegacyIOBottomSheetModal } from "../../../../utils/hooks/bottomSheet"; +import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; + +const SNAP_POINT_VALUE = 250; const ManualConfigConfirm = (): React.ReactElement => ( <> - + {I18n.t("services.optIn.preferences.manualConfig.bottomSheet.body")} - + ); @@ -23,24 +20,40 @@ export const useManualConfigBottomSheet = (onConfirm: () => void) => { present, bottomSheet: manualConfigBottomSheet, dismiss - } = useLegacyIOBottomSheetModal( - , - I18n.t("services.optIn.preferences.manualConfig.bottomSheet.title"), - 350, - dismiss()), - onPressWithGestureHandler: true - }} - rightButton={{ - ...errorButtonProps(() => { - onConfirm(); - dismiss(); - }), - onPressWithGestureHandler: true - }} - /> + } = useIOBottomSheetAutoresizableModal( + { + title: I18n.t( + "services.optIn.preferences.manualConfig.bottomSheet.title" + ), + component: , + fullScreen: true, + footer: ( + dismiss(), + accessibilityLabel: I18n.t("global.buttons.cancel") + } + }} + secondary={{ + type: "Solid", + buttonProps: { + color: "danger", + label: I18n.t("global.buttons.confirm"), + accessibilityLabel: I18n.t("global.buttons.confirm"), + onPress: () => { + onConfirm(); + dismiss(); + } + } + }} + /> + ) + }, + SNAP_POINT_VALUE ); return { present, manualConfigBottomSheet, dismiss }; diff --git a/ts/screens/profile/components/services/ServicesContactComponent.tsx b/ts/screens/profile/components/services/ServicesContactComponent.tsx index aee99302fc5..5b440c42879 100644 --- a/ts/screens/profile/components/services/ServicesContactComponent.tsx +++ b/ts/screens/profile/components/services/ServicesContactComponent.tsx @@ -1,114 +1,74 @@ -import { HSpacer, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import { constNull } from "fp-ts/lib/function"; -import * as React from "react"; -import { FlatList, ListRenderItemInfo, View } from "react-native"; -import { connect } from "react-redux"; +import React, { ReactElement, useCallback, useEffect, useState } from "react"; +import { IOColors, LabelSmall, RadioGroup } from "@pagopa/io-app-design-system"; +import { Text } from "react-native"; import { ServicesPreferencesModeEnum } from "../../../../../definitions/backend/ServicesPreferencesMode"; -import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; -import TouchableDefaultOpacity from "../../../../components/TouchableDefaultOpacity"; -import { IOBadge } from "../../../../components/core/IOBadge"; -import { H4 } from "../../../../components/core/typography/H4"; -import { H5 } from "../../../../components/core/typography/H5"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; import I18n from "../../../../i18n"; -import { Dispatch } from "../../../../store/actions/types"; -import { GlobalState } from "../../../../store/reducers/types"; +import { usePrevious } from "../../../../utils/hooks/usePrevious"; type Props = { onSelectMode: (mode: ServicesPreferencesModeEnum) => void; mode?: ServicesPreferencesModeEnum; showBadge?: boolean; -} & ReturnType & - ReturnType; - -type ContactOption = { - title: string; - mode: ServicesPreferencesModeEnum; - description1: string; - description2?: string; }; -const options = (): ReadonlyArray => [ - { - title: I18n.t("services.optIn.preferences.quickConfig.title"), - mode: ServicesPreferencesModeEnum.AUTO, - description1: I18n.t("services.optIn.preferences.quickConfig.body.text1"), - description2: I18n.t("services.optIn.preferences.quickConfig.body.text2") - }, - { - title: I18n.t("services.optIn.preferences.manualConfig.title"), - mode: ServicesPreferencesModeEnum.MANUAL, - description1: I18n.t("services.optIn.preferences.manualConfig.body.text1") - } -]; +const ServicesContactComponent = (props: Props): ReactElement => { + // We put the options inside the component to handle translations + // when the language changes, + // or is different between the device and the app. + const options = [ + { + value: I18n.t("services.optIn.preferences.quickConfig.title"), + id: ServicesPreferencesModeEnum.AUTO, + description: ( + + {I18n.t("services.optIn.preferences.quickConfig.body.text1")}{" "} + + {I18n.t("services.optIn.preferences.quickConfig.body.text2")} + + + ) + }, + { + value: I18n.t("services.optIn.preferences.manualConfig.title"), + id: ServicesPreferencesModeEnum.MANUAL, + description: I18n.t("services.optIn.preferences.manualConfig.body.text1") + } + ]; + + const { mode, onSelectMode } = props; + const [selectedItem, setSelectedItem] = useState(mode); + const prevMode = usePrevious(mode); + + const handlePress = useCallback( + (value: any) => { + // if the selected mode is the same, + // it does not have to do anything, + // otherwise it would re-run the POST /profile + if (mode !== value) { + onSelectMode(value); + } + }, + [mode, onSelectMode] + ); -const ServicesContactComponent = (props: Props): React.ReactElement => { - const renderListItem = ({ item }: ListRenderItemInfo) => { - const isSelected = item.mode === props.mode; - return ( - <> - props.onSelectMode(item.mode) - } - > - - {props.showBadge && - item.mode === ServicesPreferencesModeEnum.AUTO && ( - - )} -

{item.title}

-
- {item.description1} - {item.description2 &&
{`\n${item.description2}`}
} -

-
- - - - - - - - - - ); - }; + useEffect(() => { + if (mode !== prevMode) { + // in case "MANUAL" if the user confirms that he + // wants to use the MANUAL MODE after being + // shown the bottomsheet then the data is selected + // else the other option remains selected + setSelectedItem(mode); + } + }, [mode, prevMode]); return ( - o.mode} + + type="radioListItem" + items={options} + selectedItem={selectedItem} + onPress={handlePress} /> ); }; -const mapStateToProps = (_: GlobalState) => ({}); - -const mapDispatchToProps = (_: Dispatch) => ({}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ServicesContactComponent); +export default ServicesContactComponent; diff --git a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx index 3b0fe9fe066..0275f886944 100644 --- a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx +++ b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx @@ -1,104 +1,114 @@ -import * as React from "react"; -import { View, SafeAreaView, StyleSheet } from "react-native"; -import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import React, { useCallback, useMemo } from "react"; import * as O from "fp-ts/lib/Option"; +import { Route, useRoute } from "@react-navigation/native"; import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { H3 } from "../../../components/core/typography/H3"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import themeVariables from "../../../theme/variables"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import { CheckEmailParamsList } from "../../../navigation/params/CheckEmailParamsList"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import NavigationService from "../../../navigation/NavigationService"; +import { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; import ROUTES from "../../../navigation/routes"; -import { useIODispatch } from "../../../store/hooks"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { acknowledgeOnEmailValidation } from "../../../store/actions/profile"; +import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; +import { + trackEmailAlreadyTaken, + trackEmailDuplicateEditingConfirm +} from "../../analytics/emailAnalytics"; -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: themeVariables.contentPaddingLarge - }, - title: { - textAlign: "center" - } -}); +import { isProfileFirstOnBoardingSelector } from "../../../store/reducers/profile"; +import { getFlowType } from "../../../utils/analytics"; +import { + BodyProps, + OperationResultScreenContent +} from "../../../components/screens/OperationResultScreenContent"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; export type OnboardingServicesPreferenceScreenNavigationParams = { isFirstOnboarding: boolean; }; -type Props = IOStackNavigationRouteProps< - CheckEmailParamsList, - "CHECK_EMAIL_ALREADY_TAKEN" ->; export type EmailAlreadyUsedScreenParamList = { email: string; }; -const navigateToInsertEmailScreen = () => { - NavigationService.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN - }); +const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { + title: "email.cduScreens.emailAlreadyTaken.title", + body: "email.cduScreens.emailAlreadyTaken.help.body" }; -const EmailAlreadyTakenScreen = (props: Props) => { - const { email } = props.route.params; +const EmailAlreadyTakenScreen = () => { + const { email } = + useRoute< + Route<"CHECK_EMAIL_ALREADY_TAKEN", EmailAlreadyUsedScreenParamList> + >().params; const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const isFirstOnBoarding = useIOSelector(isProfileFirstOnBoardingSelector); + const flow = getFlowType(true, isFirstOnBoarding); + const navigateToInsertEmailScreen = useCallback(() => { + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { + isOnboarding: true + } + }); + }, [navigation]); + + useOnFirstRender(() => { + trackEmailAlreadyTaken(flow); + }); - const confirmButtonOnPress = React.useCallback(() => { + const confirmButtonOnPress = useCallback(() => { + trackEmailDuplicateEditingConfirm(flow); dispatch(acknowledgeOnEmailValidation(O.none)); navigateToInsertEmailScreen(); - }, [dispatch]); + }, [dispatch, flow, navigateToInsertEmailScreen]); - const continueButtonProps = { - onPress: confirmButtonOnPress, - title: I18n.t("email.cduScreens.emailAlreadyTaken.editButton"), - block: true - }; + const bodyPropsArray: Array = useMemo( + () => [ + { + text: I18n.t("email.cduScreens.emailAlreadyTaken.subtitleStart"), + style: { + textAlign: "center" + } + }, + { + text: <> {email} , + style: { + textAlign: "center" + }, + weight: "SemiBold" + }, + { + text: I18n.t("email.cduScreens.emailAlreadyTaken.subtitleEnd"), + style: { + textAlign: "center" + } + } + ], + [email] + ); + + useHeaderSecondLevel({ + title: "", + supportRequest: true, + canGoBack: false, + contextualHelpMarkdown + }); return ( - - - - - -

- {I18n.t("email.cduScreens.emailAlreadyTaken.title")} -

- - - - {I18n.t("email.cduScreens.emailAlreadyTaken.subtitleStart")} - - - {" "} - {" " + email + " "} - - - {I18n.t("email.cduScreens.emailAlreadyTaken.subtitleEnd")} - - -
- -
-
+ isHeaderVisible={true} + /> ); }; export default EmailAlreadyTakenScreen; diff --git a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx index 9ac5b6e87fc..ef3b7cade7e 100644 --- a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx +++ b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx @@ -1,105 +1,114 @@ -import * as React from "react"; -import { View, SafeAreaView, StyleSheet } from "react-native"; -import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import React, { useMemo, useCallback } from "react"; import * as O from "fp-ts/lib/Option"; +import { Route, useRoute } from "@react-navigation/native"; import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { H3 } from "../../../components/core/typography/H3"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import themeVariables from "../../../theme/variables"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import { Link } from "../../../components/core/typography/Link"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import NavigationService from "../../../navigation/NavigationService"; import ROUTES from "../../../navigation/routes"; -import { useIODispatch } from "../../../store/hooks"; +import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { acknowledgeOnEmailValidation } from "../../../store/actions/profile"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { CheckEmailParamsList } from "../../../navigation/params/CheckEmailParamsList"; - -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: themeVariables.contentPaddingLarge - }, - title: { - textAlign: "center" - } -}); +import { useIONavigation } from "../../../navigation/params/AppParamsList"; +import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; +import { + trackEmailNotAlreadyConfirmed, + trackSendValidationEmail +} from "../../analytics/emailAnalytics"; +import { getFlowType } from "../../../utils/analytics"; +import { isProfileFirstOnBoardingSelector } from "../../../store/reducers/profile"; +import { + BodyProps, + OperationResultScreenContent +} from "../../../components/screens/OperationResultScreenContent"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; export type EmailNotVerifiedScreenParamList = { email: string; }; -export type Props = IOStackNavigationRouteProps< - CheckEmailParamsList, - "CHECK_EMAIL_NOT_VERIFIED" ->; - -const ValidateEmailScreen = (props: Props) => { +const ValidateEmailScreen = () => { const dispatch = useIODispatch(); - - const { email } = props.route.params; - - const navigateToInsertEmailScreen = () => { - NavigationService.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN + const navigation = useIONavigation(); + const { email } = + useRoute< + Route<"CHECK_EMAIL_NOT_VERIFIED", EmailNotVerifiedScreenParamList> + >().params; + const isFirstOnboarding = useIOSelector(isProfileFirstOnBoardingSelector); + const flow = getFlowType(true, isFirstOnboarding); + const navigateToInsertEmailScreen = useCallback(() => { + navigation.navigate(ROUTES.ONBOARDING, { + screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, + params: { + isOnboarding: true + } }); + }, [navigation]); + + const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { + title: "email.cduScreens.validateMail.title", + body: "email.cduScreens.validateMail.help.body" }; - const confirmButtonOnPress = React.useCallback(() => { + useOnFirstRender(() => { + trackEmailNotAlreadyConfirmed(flow); + }); + + const confirmButtonOnPress = useCallback(() => { // We dispatch this action to show the InsertEmailScreen with // the validation modal already opened. + trackSendValidationEmail(flow); dispatch(acknowledgeOnEmailValidation(O.some(false))); navigateToInsertEmailScreen(); - }, [dispatch]); + }, [dispatch, flow, navigateToInsertEmailScreen]); - const modifyEmailButtonOnPress = React.useCallback(() => { + const modifyEmailButtonOnPress = useCallback(() => { dispatch(acknowledgeOnEmailValidation(O.none)); navigateToInsertEmailScreen(); - }, [dispatch]); + }, [dispatch, navigateToInsertEmailScreen]); - const continueButtonProps = { - onPress: confirmButtonOnPress, - title: I18n.t("email.cduScreens.validateMail.validateButton"), - block: true - }; + const bodyPropsArray: Array = useMemo( + () => [ + { + text: I18n.t("email.cduScreens.validateMail.subtitle"), + style: { + textAlign: "center" + } + }, + { + text: <> {email} , + style: { + textAlign: "center" + }, + weight: "SemiBold" + } + ], + [email] + ); + + useHeaderSecondLevel({ + title: "", + supportRequest: true, + contextualHelpMarkdown, + canGoBack: false + }); return ( - - - - - -

- {I18n.t("email.cduScreens.validateMail.title")} -

- - - {I18n.t("email.cduScreens.validateMail.subtitle")} - - {email} - - - {I18n.t("email.cduScreens.validateMail.editButton")} - -
- -
-
+ isHeaderVisible={true} + /> ); }; export default ValidateEmailScreen; diff --git a/ts/screens/profile/playgrounds/MarkdownPlayground.tsx b/ts/screens/profile/playgrounds/MarkdownPlayground.tsx index 6959f1e564e..5ce87e8819d 100644 --- a/ts/screens/profile/playgrounds/MarkdownPlayground.tsx +++ b/ts/screens/profile/playgrounds/MarkdownPlayground.tsx @@ -1,38 +1,100 @@ +import { + ButtonOutline, + ButtonSolid, + HSpacer, + IOColors, + IOVisualCostants, + IconButtonSolid, + LabelSmallAlt, + VSpacer +} from "@pagopa/io-app-design-system"; import { useLinkTo } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; -import { Content } from "native-base"; +import I18n from "i18n-js"; import React, { useCallback } from "react"; -import { View, SafeAreaView, StyleSheet, TextInput } from "react-native"; -import { Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { ScrollView, StyleSheet, TextInput, View } from "react-native"; import { MessageBodyMarkdown } from "../../../../definitions/backend/MessageBodyMarkdown"; -import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; -import { Label } from "../../../components/core/typography/Label"; import { ExtractedCtaButton } from "../../../components/cta/ExtractedCtaButton"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import Markdown from "../../../components/ui/Markdown"; +import LegacyMarkdown from "../../../components/ui/Markdown/LegacyMarkdown"; import { CTA } from "../../../features/messages/types/MessageCTA"; import { cleanMarkdownFromCTAs, getMessageCTA, handleCtaAction } from "../../../features/messages/utils/messages"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; import { maybeNotNullyString } from "../../../utils/strings"; const styles = StyleSheet.create({ - flex: { flex: 1 }, - textInput: { flex: 1, padding: 1, borderWidth: 1, height: 60 }, + textInput: { + flex: 1, + padding: 8, + borderWidth: 1, + borderRadius: 8, + borderColor: IOColors["grey-450"], + height: 64 + }, row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" }, - contentCenter: { justifyContent: "center" } + horizontalScroll: { + flexShrink: 1, + marginLeft: -IOVisualCostants.appMarginDefault, + marginRight: -IOVisualCostants.appMarginDefault, + paddingHorizontal: IOVisualCostants.appMarginDefault + } }); +const MARKDOWN_REFERENCE = I18n.t("global.markdown.reference"); + +const MARKDOWN_HEADING = `# I am a Header 1 + +## I am a Header 2 + +### I am a Header 3 + +#### I am a Header 4 + +##### I am a Header 5 + +###### I am a Header 6 +`; + +const MARKDOWN_PARAGRAPH = `A simple paragraph. + +Text can be emphasized with *asterisk* or _underscore_. + +If you need bold use **double asterisk**. +`; + +const MARKDOWN_LIST = `Unordered list: + +* React +* Vue +* Angular + +Ordered list: + +1. React +2. Vue +3. Angular +`; + const MarkdownPlayground = () => { const [markdownText, setMarkdownText] = React.useState(""); const [inputText, setInputText] = React.useState(""); + const setMarkdown = (markdownString: string) => { + setMarkdownText(markdownString); + setInputText(markdownString); + }; + + useHeaderSecondLevel({ + title: "Markdown playground" + }); + const linkTo = useLinkTo(); const handleCtaPress = useCallback( (cta: CTA) => handleCtaAction(cta, linkTo), @@ -42,74 +104,108 @@ const MarkdownPlayground = () => { const maybeCTA = getMessageCTA(markdownText as MessageBodyMarkdown); const ctaMessage = O.isSome(maybeCTA) ? `${maybeCTA.value.cta_1 ? "2" : "1"} cta found!` - : "no CTA found"; + : "No CTA found"; const isMarkdownSet = O.isSome(maybeNotNullyString(markdownText)); + return ( - - - - - + + + + + + setMarkdownText(inputText)} + accessibilityLabel="Invia" /> - - - setMarkdownText(inputText)} - > - - - - - + + + + + setMarkdown(MARKDOWN_HEADING)} + /> + + setMarkdown(MARKDOWN_PARAGRAPH)} + /> + + setMarkdown(MARKDOWN_LIST)} + /> + + setMarkdown(MARKDOWN_REFERENCE)} + /> + - - {isMarkdownSet && } + + + setMarkdown("")} + label="Clear" + accessibilityLabel="Clear" + /> + + + + {isMarkdownSet && {ctaMessage}} - {O.isSome(maybeCTA) && ( + {O.isSome(maybeCTA) && ( + + + + )} + {O.isSome(maybeCTA) && maybeCTA.value.cta_2 && ( + <> + - )} - {O.isSome(maybeCTA) && maybeCTA.value.cta_2 && ( - <> - - - - - - )} - {isMarkdownSet && ( - <> - - - {cleanMarkdownFromCTAs(markdownText as MessageBodyMarkdown)} - - - )} - - - + + )} + {isMarkdownSet && ( + <> + + + {cleanMarkdownFromCTAs(markdownText as MessageBodyMarkdown)} + + + )} + +
); }; diff --git a/ts/screens/profile/playgrounds/WalletPaymentPlayground.tsx b/ts/screens/profile/playgrounds/WalletPaymentPlayground.tsx index 0af4be895dd..f9c6c80eeab 100644 --- a/ts/screens/profile/playgrounds/WalletPaymentPlayground.tsx +++ b/ts/screens/profile/playgrounds/WalletPaymentPlayground.tsx @@ -5,33 +5,23 @@ import { } from "@pagopa/io-app-design-system"; import { RptId as PagoPaRptId, - PaymentNoticeNumberFromString, - RptIdFromString as PagoPaRptIdFromString + RptIdFromString as PagoPaRptIdFromString, + PaymentNoticeNumberFromString } from "@pagopa/io-pagopa-commons/lib/pagopa"; -import { useNavigation } from "@react-navigation/native"; -import { sequenceS } from "fp-ts/lib/Apply"; import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; -import { RptId } from "../../../../definitions/pagopa/ecommerce/RptId"; import { validateOrganizationFiscalCode, validatePaymentNoticeNumber -} from "../../../features/walletV3/common/utils/validation"; -import { WalletPaymentRoutes } from "../../../features/walletV3/payment/navigation/routes"; -import { walletPaymentInitState } from "../../../features/walletV3/payment/store/actions/orchestration"; +} from "../../../features/payments/common/utils/validation"; +import { usePagoPaPayment } from "../../../features/payments/checkout/hooks/usePagoPaPayment"; import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; import I18n from "../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../navigation/params/AppParamsList"; -import { useIODispatch } from "../../../store/hooks"; +import { generateValidRandomRptIdString } from "./utils"; const WalletPaymentPlayground = () => { - const dispatch = useIODispatch(); - const navigation = useNavigation>(); + const { startPaymentFlowWithData } = usePagoPaPayment(); const [rptId, setRptId] = React.useState(); const [paymentNoticeNumber, setPaymentNoticeNumber] = @@ -54,36 +44,22 @@ const WalletPaymentPlayground = () => { React.useEffect(() => { pipe( - sequenceS(E.Monad)({ - paymentNoticeNumber: E.right(paymentNoticeNumber), - organizationFiscalCode: E.right(organizationFiscalCode) - }), - E.chain(PagoPaRptId.decode), + PagoPaRptId.decode({ paymentNoticeNumber, organizationFiscalCode }), E.map(setRptId), E.getOrElse(() => setRptId(undefined)) ); }, [paymentNoticeNumber, organizationFiscalCode]); const navigateToWalletPayment = () => { - pipe( - rptId, - O.fromNullable, - O.map(PagoPaRptIdFromString.encode), - O.map(rptId => { - dispatch(walletPaymentInitState()); - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_DETAIL, - params: { - rptId: rptId as RptId - } - }); - }) - ); + startPaymentFlowWithData({ + paymentNoticeNumber, + organizationFiscalCode + }); }; const generateValidRandomRptId = () => { pipe( - "00000123456002160020399398578", + generateValidRandomRptIdString(), PagoPaRptIdFromString.decode, E.map(setRptId) ); diff --git a/ts/screens/profile/playgrounds/WalletPlayground.tsx b/ts/screens/profile/playgrounds/WalletPlayground.tsx index 5fe9462e3df..5f7d3f02533 100644 --- a/ts/screens/profile/playgrounds/WalletPlayground.tsx +++ b/ts/screens/profile/playgrounds/WalletPlayground.tsx @@ -1,25 +1,25 @@ /* eslint-disable sonarjs/no-identical-functions */ -import { Divider, ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; +import { + ContentWrapper, + Divider, + ListItemNav +} from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; import React from "react"; -import { ScrollView } from "react-native"; -import { Body } from "../../../components/core/typography/Body"; -import { H2 } from "../../../components/core/typography/H2"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import { WalletOnboardingRoutes } from "../../../features/walletV3/onboarding/navigation/navigator"; +import { RNavScreenWithLargeHeader } from "../../../components/ui/RNavScreenWithLargeHeader"; import { AppParamsList, IOStackNavigationProp } from "../../../navigation/params/AppParamsList"; import ROUTES from "../../../navigation/routes"; +import { PaymentsOnboardingRoutes } from "../../../features/payments/onboarding/navigation/routes"; const WalletPlayground = () => { const navigation = useNavigation>(); const navigateToWalletOnboarding = () => { - navigation.navigate(WalletOnboardingRoutes.WALLET_ONBOARDING_MAIN, { - screen: WalletOnboardingRoutes.WALLET_ONBOARDING_SELECT_PAYMENT_METHOD + navigation.navigate(PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_NAVIGATOR, { + screen: PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD }); }; @@ -30,11 +30,11 @@ const WalletPlayground = () => { }; return ( - - -

New wallet playground

- Choose the playground flow for the new wallet - + + {/* Onboarding Playground */} { description="Start the payment flow to pay with a method of payment" onPress={navigateToWalletPaymentPlayground} /> -
-
+ + ); }; diff --git a/ts/screens/profile/playgrounds/utils.ts b/ts/screens/profile/playgrounds/utils.ts new file mode 100644 index 00000000000..7829ad566e9 --- /dev/null +++ b/ts/screens/profile/playgrounds/utils.ts @@ -0,0 +1,17 @@ +const generateRandomNumberString = (length: number) => { + const characters = "0123456789"; + + return Array.from(Array(length).keys()) + .map(() => { + const randomIndex = Math.floor(Math.random() * characters.length); + return characters[randomIndex]; + }) + .join(""); +}; + +export const generateValidRandomRptIdString = () => { + const organizationFiscalCode = generateRandomNumberString(11); + const paymentNoticeNumber = "002" + generateRandomNumberString(15); + + return `${organizationFiscalCode}${paymentNoticeNumber}`; +}; diff --git a/ts/screens/services/ServiceDetailsScreen.tsx b/ts/screens/services/LegacyServiceDetailsScreen.tsx similarity index 80% rename from ts/screens/services/ServiceDetailsScreen.tsx rename to ts/screens/services/LegacyServiceDetailsScreen.tsx index a747765211f..fcfdf1e6c01 100644 --- a/ts/screens/services/ServiceDetailsScreen.tsx +++ b/ts/screens/services/LegacyServiceDetailsScreen.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from "react"; import { View, SafeAreaView } from "react-native"; import { connect } from "react-redux"; import { VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { ServiceId } from "../../../definitions/backend/ServiceId"; import { SpecialServiceMetadata } from "../../../definitions/backend/SpecialServiceMetadata"; import { IOStyles } from "../../components/core/variables/IOStyles"; @@ -20,16 +21,15 @@ import ContactPreferencesToggles from "../../components/services/ContactPreferen import ServiceMetadataComponent from "../../components/services/ServiceMetadata"; import SpecialServicesCTA from "../../components/services/SpecialServices/SpecialServicesCTA"; import TosAndPrivacyBox from "../../components/services/TosAndPrivacyBox"; -import Markdown from "../../components/ui/Markdown"; +import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; import { FooterTopShadow } from "../../components/FooterTopShadow"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { ServicesParamsList } from "../../navigation/params/ServicesParamsList"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import { loadServiceDetail } from "../../store/actions/services"; import { Dispatch } from "../../store/actions/types"; import { contentSelector } from "../../store/reducers/content"; import { isDebugModeEnabledSelector } from "../../store/reducers/debug"; -import { serviceByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { serviceByIdPotSelector } from "../../features/services/store/reducers/servicesById"; import { isEmailEnabledSelector, isInboxEnabledSelector, @@ -40,23 +40,11 @@ import { GlobalState } from "../../store/reducers/types"; import { getServiceCTA } from "../../features/messages/utils/messages"; import { logosForService } from "../../utils/services"; import { handleItemOnPress } from "../../utils/url"; - -export type ServiceDetailsScreenNavigationParams = Readonly<{ - serviceId: ServiceId; - // if true the service should be activated automatically - // as soon as the screen is shown (used for custom activation - // flows like PN) - activate?: boolean; -}>; - -type OwnProps = IOStackNavigationRouteProps< - ServicesParamsList, - "SERVICE_DETAIL" ->; +import { useIOSelector } from "../../store/hooks"; +import { ServiceDetailsScreenNavigationParams } from "../../features/services/screens/ServiceDetailsScreen"; type Props = ReturnType & - ReturnType & - OwnProps; + ReturnType; type SpecialServiceWrapper = { isSpecialService: boolean; @@ -72,10 +60,21 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * Screen displaying the details of a selected service. The user * can enable/disable the service and customize the notification settings. */ -const ServiceDetailsScreen = (props: Props) => { +const LegacyServiceDetailsScreen = (props: Props) => { const [isMarkdownLoaded, setIsMarkdownLoaded] = useState(false); + const navigation = useIONavigation(); + const { serviceId, activate } = + useRoute>() + .params; + + const service = useIOSelector(state => + pipe(serviceByIdPotSelector(state, serviceId), pot.toUndefined) + ); - const { service, serviceId, loadServiceDetail } = props; + // const serviceId = props.route.params.serviceId; + // const activate = props.route.params.activate; + + const { loadServiceDetail } = props; useEffect(() => { loadServiceDetail(serviceId); }, [serviceId, loadServiceDetail]); @@ -109,7 +108,7 @@ const ServiceDetailsScreen = (props: Props) => { return ( props.navigation.goBack()} + goBack={() => navigation.goBack()} headerTitle={I18n.t("serviceDetail.headerTitle")} contextualHelpMarkdown={contextualHelpMarkdown} faqCategories={["services_detail"]} @@ -127,13 +126,13 @@ const ServiceDetailsScreen = (props: Props) => { {metadata?.description && ( <> - {metadata.description} - + )} @@ -190,11 +189,11 @@ const ServiceDetailsScreen = (props: Props) => { <> )} @@ -205,22 +204,14 @@ const ServiceDetailsScreen = (props: Props) => { ); }; -const mapStateToProps = (state: GlobalState, props: OwnProps) => { - const serviceId = props.route.params.serviceId; - const activate = props.route.params.activate; - - return { - serviceId, - activate, - service: pipe(serviceByIdSelector(state, serviceId), pot.toUndefined), - isInboxEnabled: isInboxEnabledSelector(state), - isEmailEnabled: isEmailEnabledSelector(state), - isEmailValidated: isProfileEmailValidatedSelector(state), - content: contentSelector(state), - profile: profileSelector(state), - isDebugModeEnabled: isDebugModeEnabledSelector(state) - }; -}; +const mapStateToProps = (state: GlobalState) => ({ + isInboxEnabled: isInboxEnabledSelector(state), + isEmailEnabled: isEmailEnabledSelector(state), + isEmailValidated: isProfileEmailValidatedSelector(state), + content: contentSelector(state), + profile: profileSelector(state), + isDebugModeEnabled: isDebugModeEnabledSelector(state) +}); const mapDispatchToProps = (dispatch: Dispatch) => ({ loadServiceDetail: (id: ServiceId) => dispatch(loadServiceDetail.request(id)), @@ -230,4 +221,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ export default connect( mapStateToProps, mapDispatchToProps -)(ServiceDetailsScreen); +)(LegacyServiceDetailsScreen); diff --git a/ts/screens/services/ServicesHomeScreen.tsx b/ts/screens/services/ServicesHomeScreen.tsx index 3efe85ba3a3..7be05f283d5 100644 --- a/ts/screens/services/ServicesHomeScreen.tsx +++ b/ts/screens/services/ServicesHomeScreen.tsx @@ -76,7 +76,7 @@ import { visibleServicesDetailLoadStateSelector } from "../../store/reducers/entities/services"; import { readServicesByIdSelector } from "../../store/reducers/entities/services/readStateByServiceId"; -import { servicesByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { servicesByIdSelector } from "../../features/services/store/reducers/servicesById"; import { visibleServicesSelector } from "../../store/reducers/entities/services/visibleServices"; import { wasServiceAlertDisplayedOnceSelector } from "../../store/reducers/persistedPreferences"; import { profileSelector, ProfileState } from "../../store/reducers/profile"; @@ -95,7 +95,7 @@ import { getProfileChannelsforServicesList } from "../../utils/profile"; import { showToast } from "../../utils/showToast"; -import { ServiceDetailsScreenNavigationParams } from "./ServiceDetailsScreen"; +import { ServiceDetailsScreenNavigationParams } from "../../features/services/screens/ServiceDetailsScreen"; type OwnProps = IOStackNavigationRouteProps; @@ -234,7 +234,11 @@ class ServicesHomeScreen extends React.Component { public componentDidUpdate(prevProps: Props, prevState: State) { // if some errors occur while updating profile, we will show a message in a toast // profile could be updated by enabling/disabling on or more channel of a service - if (pot.isError(this.props.profile) && !pot.isError(prevProps.profile)) { + if ( + pot.isError(this.props.profile) && + !pot.isError(prevProps.profile) && + this.props.profile.error.type !== "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR" + ) { showToast( I18n.t("serviceDetail.onUpdateEnabledChannelsFailure"), "danger" diff --git a/ts/screens/services/ServicesLocalScreen.tsx b/ts/screens/services/ServicesLocalScreen.tsx index 0de66a244f9..1f9bc5cc5d0 100644 --- a/ts/screens/services/ServicesLocalScreen.tsx +++ b/ts/screens/services/ServicesLocalScreen.tsx @@ -1,13 +1,12 @@ -import { useNavigation } from "@react-navigation/native"; -import * as React from "react"; -import { useCallback } from "react"; +import React, { useCallback } from "react"; import { View, StyleSheet } from "react-native"; import { ServicePublic } from "../../../definitions/backend/ServicePublic"; import { IOStyles } from "../../components/core/variables/IOStyles"; import LocalServicesWebView from "../../components/services/LocalServicesWebView"; -import ROUTES from "../../navigation/routes"; import { showServiceDetails } from "../../store/actions/services"; import { useIODispatch } from "../../store/hooks"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; const styles = StyleSheet.create({ contentWrapper: { @@ -17,14 +16,14 @@ const styles = StyleSheet.create({ const ServicesLocalScreen = () => { const dispatch = useIODispatch(); - const navigation = useNavigation(); + const navigation = useIONavigation(); const onServiceSelect = useCallback( (service: ServicePublic) => { // when a service gets selected the service is recorded as read dispatch(showServiceDetails(service)); - navigation.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICE_DETAIL, + navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICE_DETAIL, params: { serviceId: service.service_id } }); }, diff --git a/ts/screens/services/ServicesNationalScreen.tsx b/ts/screens/services/ServicesNationalScreen.tsx index 0825e982d96..c2e1a43020b 100644 --- a/ts/screens/services/ServicesNationalScreen.tsx +++ b/ts/screens/services/ServicesNationalScreen.tsx @@ -1,11 +1,9 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useNavigation } from "@react-navigation/native"; import * as React from "react"; import { useCallback } from "react"; import { Animated } from "react-native"; import { ServicePublic } from "../../../definitions/backend/ServicePublic"; import ServicesTab from "../../components/services/ServicesTab"; -import ROUTES from "../../navigation/routes"; import { loadVisibleServices, showServiceDetails @@ -17,11 +15,13 @@ import { visibleServicesDetailLoadStateSelector } from "../../store/reducers/entities/services"; import { userMetadataSelector } from "../../store/reducers/userMetadata"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; const tabScrollOffset = new Animated.Value(0); const ServicesNationalScreen = () => { - const navigation = useNavigation(); + const navigation = useIONavigation(); const dispatch = useIODispatch(); const nationalTabSections = useIOSelector(nationalServicesSectionsSelector); const visibleServicesContentLoadState = useIOSelector( @@ -44,8 +44,8 @@ const ServicesNationalScreen = () => { (service: ServicePublic) => { // when a service gets selected the service is recorded as read dispatch(showServiceDetails(service)); - navigation.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICE_DETAIL, + navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICE_DETAIL, params: { serviceId: service.service_id } }); }, diff --git a/ts/screens/services/ServicesWebviewScreen.tsx b/ts/screens/services/ServicesWebviewScreen.tsx index 62d02536bed..085d0a8e9ae 100644 --- a/ts/screens/services/ServicesWebviewScreen.tsx +++ b/ts/screens/services/ServicesWebviewScreen.tsx @@ -17,7 +17,7 @@ import { tokenFromNameSelector, TokenName } from "../../store/reducers/authentication"; -import { serviceMetadataByIdSelector } from "../../store/reducers/entities/services/servicesById"; +import { serviceMetadataByIdSelector } from "../../features/services/store/reducers/servicesById"; export type ServiceWebviewScreenNavigationParams = Readonly<{ serviceId: ServiceId; @@ -37,8 +37,8 @@ const ServicesWebviewScreen: React.FunctionComponent = ( useRoute< Route<"SERVICES_NAVIGATOR", ServiceWebviewScreenNavigationParams> >(); - const maybeService = useIOSelector( - serviceMetadataByIdSelector(route.params.serviceId) + const maybeService = useIOSelector(state => + serviceMetadataByIdSelector(state, route.params.serviceId) ); const token = useIOSelector( tokenFromNameSelector(maybeService?.token_name as TokenName) diff --git a/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx b/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx index 38fef4fdd5d..e9b5d628562 100644 --- a/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx +++ b/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx @@ -10,12 +10,12 @@ import { applicationChangeState } from "../../../store/actions/application"; import { appReducer } from "../../../store/reducers"; import { GlobalState } from "../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper"; -import ServiceDetailsScreen from "../ServiceDetailsScreen"; -import ROUTES from "../../../navigation/routes"; +import ServiceDetailsScreen from "../LegacyServiceDetailsScreen"; import { loadServiceDetail, loadVisibleServices } from "../../../store/actions/services"; +import { SERVICES_ROUTES } from "../../../features/services/navigation/routes"; jest.useFakeTimers(); @@ -73,7 +73,7 @@ const renderComponent = () => { return { component: renderScreenWithNavigationStoreContext( ServiceDetailsScreen, - ROUTES.SERVICE_DETAIL, + SERVICES_ROUTES.SERVICE_DETAIL, { serviceId: service.service_id }, store ), diff --git a/ts/screens/wallet/AddCardScreen.tsx b/ts/screens/wallet/AddCardScreen.tsx index a88f174d539..37b3856d293 100644 --- a/ts/screens/wallet/AddCardScreen.tsx +++ b/ts/screens/wallet/AddCardScreen.tsx @@ -11,8 +11,8 @@ import { Content } from "native-base"; import React, { useState } from "react"; import { Keyboard, SafeAreaView, ScrollView, StyleSheet } from "react-native"; import { Col, Grid } from "react-native-easy-grid"; -import { connect } from "react-redux"; import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse"; import { Link } from "../../components/core/typography/Link"; import { LabelledItem } from "../../components/LabelledItem"; @@ -22,14 +22,8 @@ import BaseScreenComponent, { import SectionStatusComponent from "../../components/SectionStatus"; import FooterWithButtons from "../../components/ui/FooterWithButtons"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../navigation/params/WalletParamsList"; -import { - navigateBack, - navigateToWalletConfirmCardDetails -} from "../../store/actions/navigation"; -import { Dispatch } from "../../store/actions/types"; -import { GlobalState } from "../../store/reducers/types"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; +import { navigateToWalletConfirmCardDetails } from "../../store/actions/navigation"; import { CreditCard } from "../../types/pagopa"; import { ComponentProps } from "../../types/react"; import { useScreenReaderEnabled } from "../../utils/accessibility"; @@ -64,15 +58,6 @@ export type AddCardScreenNavigationParams = Readonly<{ keyFrom?: string; }>; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_ADD_CARD" ->; - -type Props = ReturnType & - ReturnType & - OwnProps; - const styles = StyleSheet.create({ creditCardForm: { height: 24, @@ -215,11 +200,21 @@ const getAccessibilityLabels = (creditCard: CreditCardState) => ({ : I18n.t("wallet.dummyCard.accessibility.securityCode.4D.error") }); -const AddCardScreen: React.FC = props => { +const AddCardScreen: React.FC = () => { const [creditCard, setCreditCard] = useState( INITIAL_CARD_FORM_STATE ); + const navigation = useIONavigation(); + const { inPayment, keyFrom } = + useRoute>().params; + const navigateToConfirmCardDetailsScreen = (creditCard: CreditCard) => + navigateToWalletConfirmCardDetails({ + creditCard, + inPayment, + keyFrom + }); + const isCardHolderValid = O.isNone(creditCard.holder) ? undefined : isValidCardHolder(creditCard.holder); @@ -263,7 +258,7 @@ const AddCardScreen: React.FC = props => { const secondaryButtonProps = { block: true, bordered: true, - onPress: props.navigateBack, + onPress: navigation.goBack, title: I18n.t("global.buttons.back") }; @@ -442,7 +437,7 @@ const AddCardScreen: React.FC = props => { leftButton={secondaryButtonProps} rightButton={usePrimaryButtonPropsFromState( creditCard, - props.navigateToConfirmCardDetailsScreen, + navigateToConfirmCardDetailsScreen, isValidCardHolder(creditCard.holder), O.toUndefined(maybeCreditCardValidOrExpired(creditCard)) )} @@ -452,19 +447,8 @@ const AddCardScreen: React.FC = props => { ); }; -const mapStateToProps = (_: GlobalState) => ({}); - -const mapDispatchToProps = (_: Dispatch, props: OwnProps) => ({ - navigateBack: () => navigateBack(), - navigateToConfirmCardDetailsScreen: (creditCard: CreditCard) => - navigateToWalletConfirmCardDetails({ - creditCard, - inPayment: props.route.params.inPayment, - keyFrom: props.route.params.keyFrom - }) -}); +export default AddCardScreen; -export default connect(mapStateToProps, mapDispatchToProps)(AddCardScreen); // keep encapsulation strong export const testableAddCardScreen = isTestEnv ? { isCreditCardDateExpiredOrInvalid } diff --git a/ts/screens/wallet/AddPaymentMethodScreen.tsx b/ts/screens/wallet/AddPaymentMethodScreen.tsx index d3514079de0..2445cda4804 100644 --- a/ts/screens/wallet/AddPaymentMethodScreen.tsx +++ b/ts/screens/wallet/AddPaymentMethodScreen.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { View, SafeAreaView } from "react-native"; import { connect } from "react-redux"; import { VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse"; import BpayLogo from "../../../img/wallet/payment-methods/bancomat_pay.svg"; import CreditCard from "../../../img/wallet/payment-methods/creditcard.svg"; @@ -27,8 +28,6 @@ import { walletAddPaypalStart } from "../../features/wallet/onboarding/paypal/store/actions"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../navigation/params/WalletParamsList"; import { navigateBack, navigateToWalletAddCreditCard @@ -55,13 +54,7 @@ export type AddPaymentMethodScreenNavigationParams = Readonly<{ keyFrom?: string; }>; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "WALLET_ADD_PAYMENT_METHOD" ->; - -type Props = OwnProps & - ReturnType & +type Props = ReturnType & ReturnType; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { @@ -76,13 +69,14 @@ const getPaymentMethods = ( isPaymentOnGoing: boolean; isPaypalEnabled: boolean; canOnboardBPay: boolean; - } + }, + navigateToAddCreditCard: () => void ): ReadonlyArray => [ { name: I18n.t("wallet.methods.card.name"), description: I18n.t("wallet.methods.card.description"), icon: CreditCard, - onPress: props.navigateToAddCreditCard, + onPress: navigateToAddCreditCard, status: "implemented", section: "credit_card" }, @@ -149,9 +143,16 @@ const getPaymentMethods = ( const AddPaymentMethodScreen: React.FunctionComponent = ( props: Props ) => { - const inPayment = props.route.params.inPayment; - const canAddOnlyPayablePaymentMethod = - props.route.params.showOnlyPayablePaymentMethods; + const { inPayment, showOnlyPayablePaymentMethods, keyFrom } = + useRoute< + Route<"WALLET_ADD_PAYMENT_METHOD", AddPaymentMethodScreenNavigationParams> + >().params; + + const navigateToAddCreditCard = () => + navigateToWalletAddCreditCard({ + inPayment, + keyFrom + }); const cancelButtonProps = { block: true, @@ -187,26 +188,34 @@ const AddPaymentMethodScreen: React.FunctionComponent = ( {/* since we're paying show only those method can pay with pagoPA */}
) : ( )} @@ -219,17 +228,12 @@ const AddPaymentMethodScreen: React.FunctionComponent = ( ); }; -const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => ({ +const mapDispatchToProps = (dispatch: Dispatch) => ({ navigateBack: () => navigateBack(), startBPayOnboarding: () => dispatch(walletAddBPayStart()), startPaypalOnboarding: (onOboardingCompleted: OnOnboardingCompleted) => dispatch(walletAddPaypalStart(onOboardingCompleted)), - startAddBancomat: () => dispatch(walletAddBancomatStart()), - navigateToAddCreditCard: () => - navigateToWalletAddCreditCard({ - inPayment: props.route.params.inPayment, - keyFrom: props.route.params.keyFrom - }) + startAddBancomat: () => dispatch(walletAddBancomatStart()) }); const mapStateToProps = (state: GlobalState) => { diff --git a/ts/screens/wallet/ConfirmCardDetailsScreen.tsx b/ts/screens/wallet/ConfirmCardDetailsScreen.tsx index 41203494a06..2e01409ce06 100644 --- a/ts/screens/wallet/ConfirmCardDetailsScreen.tsx +++ b/ts/screens/wallet/ConfirmCardDetailsScreen.tsx @@ -12,6 +12,7 @@ import { View, Alert, SafeAreaView, StyleSheet } from "react-native"; import { connect } from "react-redux"; import { HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { useNavigation, useRoute, Route } from "@react-navigation/native"; import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse"; import { TypeEnum } from "../../../definitions/pagopa/Wallet"; import image from "../../../img/wallet/errors/payment-unavailable-icon.png"; @@ -41,7 +42,10 @@ import { isReady } from "../../common/model/RemoteValue"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { + IOStackNavigationProp, + IOStackNavigationRouteProps +} from "../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../navigation/params/WalletParamsList"; import { navigateToAddCreditCardOutcomeCode, @@ -67,6 +71,7 @@ import { CreditCard, Wallet } from "../../types/pagopa"; import { getLocalePrimaryWithFallback } from "../../utils/locale"; import { getLookUpIdPO } from "../../utils/pmLookUpId"; import { showToast } from "../../utils/showToast"; +import { LightModalContext } from "../../components/ui/LightModal"; import { dispatchPickPspOrConfirm } from "./payment/common"; export type ConfirmCardDetailsScreenNavigationParams = Readonly<{ @@ -504,8 +509,32 @@ const mergeProps = ( }; }; -export default connect( +const ConnectedConfirmCardDetailsScreen = connect( mapStateToProps, mapDispatchToProps, mergeProps )(withLoadingSpinner(ConfirmCardDetailsScreen)); + +const ConfirmCardDetailsScreenFC = () => { + const { ...modalContext } = React.useContext(LightModalContext); + const navigation = + useNavigation< + IOStackNavigationProp + >(); + const route = + useRoute< + Route< + "WALLET_CONFIRM_CARD_DETAILS", + ConfirmCardDetailsScreenNavigationParams + > + >(); + return ( + + ); +}; + +export default ConfirmCardDetailsScreenFC; diff --git a/ts/screens/wallet/PaymentHistoryDetailsScreen.tsx b/ts/screens/wallet/PaymentHistoryDetailsScreen.tsx index da733dcbeb1..c8f07cc2978 100644 --- a/ts/screens/wallet/PaymentHistoryDetailsScreen.tsx +++ b/ts/screens/wallet/PaymentHistoryDetailsScreen.tsx @@ -5,6 +5,7 @@ import { Text as NBButtonText } from "native-base"; import { View } from "react-native"; import { connect } from "react-redux"; import { Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { useNavigation, useRoute, Route } from "@react-navigation/native"; import { EnteBeneficiario } from "../../../definitions/backend/EnteBeneficiario"; import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse"; import { ToolEnum } from "../../../definitions/content/AssistanceToolConfig"; @@ -27,7 +28,10 @@ import { zendeskSupportStart } from "../../features/zendesk/store/actions"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { + IOStackNavigationProp, + IOStackNavigationRouteProps +} from "../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../navigation/params/WalletParamsList"; import { Dispatch } from "../../store/actions/types"; import { canShowHelpSelector } from "../../store/reducers/assistanceTools"; @@ -422,7 +426,28 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(zendeskSelectedCategory(category)) }); -export default connect( +const ConnectedPaymentHistoryDetailsScreen = connect( mapStateToProps, mapDispatchToProps )(PaymentHistoryDetailsScreen); + +const PaymentHistoryDetailsScreenFC = () => { + const navigation = + useNavigation< + IOStackNavigationProp + >(); + const route = + useRoute< + Route< + "PAYMENT_HISTORY_DETAIL_INFO", + PaymentHistoryDetailsScreenNavigationParams + > + >(); + return ( + + ); +}; +export default PaymentHistoryDetailsScreenFC; diff --git a/ts/screens/wallet/TransactionDetailsScreen.tsx b/ts/screens/wallet/TransactionDetailsScreen.tsx index 0d733996dc5..a58e742dda5 100644 --- a/ts/screens/wallet/TransactionDetailsScreen.tsx +++ b/ts/screens/wallet/TransactionDetailsScreen.tsx @@ -12,24 +12,29 @@ import { } from "react-native"; import { connect } from "react-redux"; import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useNavigation, useRoute } from "@react-navigation/native"; import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; import CopyButtonComponent from "../../components/CopyButtonComponent"; import { Body } from "../../components/core/typography/Body"; import { H2 } from "../../components/core/typography/H2"; import { Link } from "../../components/core/typography/Link"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import { withLightModalContext } from "../../components/helpers/withLightModalContext"; -import { withLoadingSpinner } from "../../components/helpers/withLoadingSpinner"; import ItemSeparatorComponent from "../../components/ItemSeparatorComponent"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import FocusAwareStatusBar from "../../components/ui/FocusAwareStatusBar"; -import { LightModalContextInterface } from "../../components/ui/LightModal"; +import { + LightModalContext, + LightModalContextInterface +} from "../../components/ui/LightModal"; import { PaymentSummaryComponent } from "../../components/wallet/PaymentSummaryComponent"; import { SlidedContentComponent } from "../../components/wallet/SlidedContentComponent"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { + IOStackNavigationProp, + IOStackNavigationRouteProps +} from "../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../navigation/params/WalletParamsList"; import { Dispatch } from "../../store/actions/types"; import { backToEntrypointPayment } from "../../store/actions/wallet/payment"; @@ -46,6 +51,7 @@ import { getTransactionIUV } from "../../utils/payment"; import { formatNumberCentsToAmount } from "../../utils/stringBuilder"; +import { withLoadingSpinner } from "../../components/helpers/withLoadingSpinner"; export type TransactionDetailsScreenNavigationParams = Readonly<{ isPaymentCompletedTransaction: boolean; @@ -58,8 +64,10 @@ type OwnProps = IOStackNavigationRouteProps< >; type Props = ReturnType & - ReturnType & - LightModalContextInterface & + ReturnType; + +type TransactionDetailsScreenProps = LightModalContextInterface & + Props & OwnProps; /** @@ -89,10 +97,13 @@ type State = { * a list of information available about a * specific transaction. */ -class TransactionDetailsScreen extends React.Component { +class TransactionDetailsScreen extends React.Component< + TransactionDetailsScreenProps, + State +> { private subscription: NativeEventSubscription | undefined; private navigationEventUnsubscribe!: () => void; - constructor(props: Props) { + constructor(props: TransactionDetailsScreenProps) { super(props); this.state = { showFullReason: false }; } @@ -361,7 +372,31 @@ const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { }; }; -export default connect( +const ConnectedTransactionDetailsScreen = connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(withLoadingSpinner(TransactionDetailsScreen))); +)(withLoadingSpinner(TransactionDetailsScreen)); + +const TransactionDetailsScreenFC = () => { + const { ...modalContext } = React.useContext(LightModalContext); + const navigation = + useNavigation< + IOStackNavigationProp + >(); + const route = + useRoute< + Route< + "WALLET_TRANSACTION_DETAILS", + TransactionDetailsScreenNavigationParams + > + >(); + return ( + + ); +}; + +export default TransactionDetailsScreenFC; diff --git a/ts/screens/wallet/WalletHomeScreen.tsx b/ts/screens/wallet/WalletHomeScreen.tsx index 30b99d3e879..d837cfe5e5e 100644 --- a/ts/screens/wallet/WalletHomeScreen.tsx +++ b/ts/screens/wallet/WalletHomeScreen.tsx @@ -47,8 +47,8 @@ import { idPayWalletInitiativeListSelector } from "../../features/idpay/wallet/s import NewPaymentMethodAddedNotifier from "../../features/wallet/component/NewMethodAddedNotifier"; import FeaturedCardCarousel from "../../features/wallet/component/card/FeaturedCardCarousel"; import WalletV2PreviewCards from "../../features/wallet/component/card/WalletV2PreviewCards"; -import { WalletBarcodeRoutes } from "../../features/walletV3/barcode/navigation/routes"; -import { WalletTransactionRoutes } from "../../features/walletV3/transaction/navigation/navigator"; +import { PaymentsBarcodeRoutes } from "../../features/payments/barcode/navigation/routes"; +import { PaymentsTransactionRoutes } from "../../features/payments/transaction/navigation/routes"; import I18n from "../../i18n"; import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; import { MainTabParamsList } from "../../navigation/params/MainTabParamsList"; @@ -389,9 +389,9 @@ class WalletHomeScreen extends React.PureComponent { ) => { if (this.props.isDesignSystemEnabled) { this.props.navigation.navigate( - WalletTransactionRoutes.WALLET_TRANSACTION_MAIN, + PaymentsTransactionRoutes.PAYMENT_TRANSACTION_NAVIGATOR, { - screen: WalletTransactionRoutes.WALLET_TRANSACTION_DETAILS, + screen: PaymentsTransactionRoutes.PAYMENT_TRANSACTION_DETAILS, params: { transactionId: transaction.id } @@ -420,9 +420,12 @@ class WalletHomeScreen extends React.PureComponent { } private navigateToPaymentScanQrCode = () => { - this.props.navigation.navigate(WalletBarcodeRoutes.WALLET_BARCODE_MAIN, { - screen: WalletBarcodeRoutes.WALLET_BARCODE_SCAN - }); + this.props.navigation.navigate( + PaymentsBarcodeRoutes.PAYMENT_BARCODE_NAVIGATOR, + { + screen: PaymentsBarcodeRoutes.PAYMENT_BARCODE_SCAN + } + ); }; private footerButton(potWallets: pot.Pot, Error>) { diff --git a/ts/screens/wallet/__tests__/AddCardScreen.test.tsx b/ts/screens/wallet/__tests__/AddCardScreen.test.tsx index ca0cda09d50..0ccd35a5063 100644 --- a/ts/screens/wallet/__tests__/AddCardScreen.test.tsx +++ b/ts/screens/wallet/__tests__/AddCardScreen.test.tsx @@ -141,19 +141,22 @@ describe("getPaymentMethods", () => { startBPayOnboarding: jest.fn(), startPaypalOnboarding: jest.fn(), startAddBancomat: jest.fn(), - navigateToAddCreditCard: jest.fn(), isPaypalAlreadyAdded: true, isPaypalEnabled: true, canOnboardBPay: false, canPayWithBPay: false }; // TODO: ⚠️ cast to any only to complete the merge, should be removed! - const methods = testableFunctions.getPaymentMethods!(props as any, { - onlyPaymentMethodCanPay: true, - isPaymentOnGoing: true, - isPaypalEnabled: true, - canOnboardBPay: true - }); + const methods = testableFunctions.getPaymentMethods!( + props as any, + { + onlyPaymentMethodCanPay: true, + isPaymentOnGoing: true, + isPaypalEnabled: true, + canOnboardBPay: true + }, + jest.fn() + ); const getMethodStatus = ( methods: ReadonlyArray, @@ -174,12 +177,16 @@ describe("getPaymentMethods", () => { it("paypal should be always notImplemented when the FF is OFF", () => { // TODO: ⚠️ cast to any only to complete the merge, should be removed! - const methods = testableFunctions.getPaymentMethods!(props as any, { - onlyPaymentMethodCanPay: true, - isPaymentOnGoing: true, - isPaypalEnabled: false, - canOnboardBPay: true - }); + const methods = testableFunctions.getPaymentMethods!( + props as any, + { + onlyPaymentMethodCanPay: true, + isPaymentOnGoing: true, + isPaypalEnabled: false, + canOnboardBPay: true + }, + jest.fn() + ); expect( getMethodStatus(methods, I18n.t("wallet.methods.paypal.name")) ).toEqual("notImplemented"); @@ -187,12 +194,16 @@ describe("getPaymentMethods", () => { it("bpay should be always notImplemented if Bpay onboarding FF is OFF", () => { // TODO: ⚠️ cast to any only to complete the merge, should be removed! - const methods = testableFunctions.getPaymentMethods!(props as any, { - onlyPaymentMethodCanPay: true, - isPaymentOnGoing: true, - isPaypalEnabled: true, - canOnboardBPay: false - }); + const methods = testableFunctions.getPaymentMethods!( + props as any, + { + onlyPaymentMethodCanPay: true, + isPaymentOnGoing: true, + isPaypalEnabled: true, + canOnboardBPay: false + }, + jest.fn() + ); expect( getMethodStatus(methods, I18n.t("wallet.methods.bancomatPay.name")) ).toEqual("notImplemented"); @@ -200,12 +211,16 @@ describe("getPaymentMethods", () => { it("bpay should be always implemented if Bpay onboarding FF is ON and onlyPaymentMethodCanPay flag is OFF", () => { // TODO: ⚠️ cast to any only to complete the merge, should be removed! - const methods = testableFunctions.getPaymentMethods!(props as any, { - onlyPaymentMethodCanPay: false, - isPaymentOnGoing: true, - isPaypalEnabled: true, - canOnboardBPay: true - }); + const methods = testableFunctions.getPaymentMethods!( + props as any, + { + onlyPaymentMethodCanPay: false, + isPaymentOnGoing: true, + isPaypalEnabled: true, + canOnboardBPay: true + }, + jest.fn() + ); expect( getMethodStatus(methods, I18n.t("wallet.methods.bancomatPay.name")) ).toEqual("implemented"); @@ -222,7 +237,8 @@ describe("getPaymentMethods", () => { isPaymentOnGoing: true, isPaypalEnabled: true, canOnboardBPay: canPayWithBPay && canOnboardBPay - } + }, + jest.fn() ); expect( getMethodStatus(methods, I18n.t("wallet.methods.bancomatPay.name")) @@ -240,7 +256,8 @@ describe("getPaymentMethods", () => { isPaymentOnGoing: false, isPaypalEnabled: true, canOnboardBPay: canPayWithBPay && canOnboardBPay - } + }, + jest.fn() ); expect( getMethodStatus(methods, I18n.t("wallet.methods.bancomatPay.name")) diff --git a/ts/screens/wallet/__tests__/ConfirmCardDetailScreen.test.tsx b/ts/screens/wallet/__tests__/ConfirmCardDetailScreen.test.tsx index dd42c54e52d..2dc4002a935 100644 --- a/ts/screens/wallet/__tests__/ConfirmCardDetailScreen.test.tsx +++ b/ts/screens/wallet/__tests__/ConfirmCardDetailScreen.test.tsx @@ -67,9 +67,7 @@ const getComponent = () => { } as ConfirmCardDetailsScreenNavigationParams; const ToBeTested: React.FunctionComponent< React.ComponentProps - > = (props: React.ComponentProps) => ( - - ); + > = () => ; const globalState = appReducer(undefined, applicationChangeState("active")); const store = createStore(appReducer, globalState as any); diff --git a/ts/screens/wallet/creditCardOnboardingAttempts/CreditCardOnboardingAttemptDetailScreen.tsx b/ts/screens/wallet/creditCardOnboardingAttempts/CreditCardOnboardingAttemptDetailScreen.tsx index 21b4b83a523..fdf3a62b1ae 100644 --- a/ts/screens/wallet/creditCardOnboardingAttempts/CreditCardOnboardingAttemptDetailScreen.tsx +++ b/ts/screens/wallet/creditCardOnboardingAttempts/CreditCardOnboardingAttemptDetailScreen.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { View, StyleSheet } from "react-native"; import { useDispatch } from "react-redux"; import { Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { ToolEnum } from "../../../../definitions/content/AssistanceToolConfig"; import ButtonDefaultOpacity from "../../../components/ButtonDefaultOpacity"; import { Body } from "../../../components/core/typography/Body"; @@ -20,8 +21,7 @@ import { zendeskSupportStart } from "../../../features/zendesk/store/actions"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../store/hooks"; import { canShowHelpSelector } from "../../../store/reducers/assistanceTools"; import { assistanceToolConfigSelector } from "../../../store/reducers/backendStatus"; @@ -43,11 +43,6 @@ export type CreditCardOnboardingAttemptDetailScreenNavigationParams = Readonly<{ attempt: CreditCardInsertion; }>; -type Props = IOStackNavigationRouteProps< - WalletParamsList, - "CREDIT_CARD_ONBOARDING_ATTEMPT_DETAIL" ->; - const styles = StyleSheet.create({ row: { flexDirection: "row", @@ -78,9 +73,16 @@ const renderRow = (label: string, value: string) => ( * This screen shows credit card onboarding attempt details and allows the user * to ask assistance about this attempts */ -const CreditCardOnboardingAttemptDetailScreen = (props: Props) => { +const CreditCardOnboardingAttemptDetailScreen = () => { const dispatch = useDispatch(); - const attempt = props.route.params.attempt; + const { attempt } = + useRoute< + Route< + "CREDIT_CARD_ONBOARDING_ATTEMPT_DETAIL", + CreditCardOnboardingAttemptDetailScreenNavigationParams + > + >().params; + const navigation = useIONavigation(); const assistanceToolConfig = useIOSelector(assistanceToolConfigSelector); const outcomeCodes = useIOSelector(outcomeCodesSelector); const choosenTool = assistanceToolRemoteConfig(assistanceToolConfig); @@ -160,7 +162,7 @@ const CreditCardOnboardingAttemptDetailScreen = (props: Props) => { : undefined; return ( props.navigation.goBack()} + goBack={() => navigation.goBack()} showChat={false} dark={true} headerTitle={I18n.t("wallet.creditCard.onboardingAttempts.title")} diff --git a/ts/screens/wallet/payment/ConfirmPaymentMethodScreen.tsx b/ts/screens/wallet/payment/ConfirmPaymentMethodScreen.tsx index d0cc490ec0e..03ade8ddeef 100644 --- a/ts/screens/wallet/payment/ConfirmPaymentMethodScreen.tsx +++ b/ts/screens/wallet/payment/ConfirmPaymentMethodScreen.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { View, Alert, SafeAreaView, StyleSheet, Text } from "react-native"; import { connect } from "react-redux"; import { IOColors, Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { ImportoEuroCents } from "../../../../definitions/backend/ImportoEuroCents"; import { PaymentRequestsGetResponse } from "../../../../definitions/backend/PaymentRequestsGetResponse"; import { PspData } from "../../../../definitions/pagopa/PspData"; @@ -18,13 +19,14 @@ import { H3 } from "../../../components/core/typography/H3"; import { H4 } from "../../../components/core/typography/H4"; import { LabelSmall } from "../../../components/core/typography/LabelSmall"; import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { withLightModalContext } from "../../../components/helpers/withLightModalContext"; -import { withLoadingSpinner } from "../../../components/helpers/withLoadingSpinner"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import { LightModalContextInterface } from "../../../components/ui/LightModal"; +import { + LightModalContext, + LightModalContextInterface +} from "../../../components/ui/LightModal"; import { getCardIconFromBrandLogo } from "../../../components/wallet/card/Logo"; import { PayWebViewModal } from "../../../components/wallet/PayWebViewModal"; import { SelectionBox } from "../../../components/wallet/SelectionBox"; @@ -38,8 +40,7 @@ import { } from "../../../common/model/RemoteValue"; import { BrandImage } from "../../../features/wallet/component/card/BrandImage"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; +import { useIONavigation } from "../../../navigation/params/AppParamsList"; import ROUTES from "../../../navigation/routes"; import { navigateToPaymentOutcomeCode, @@ -90,6 +91,7 @@ import { getLookUpIdPO } from "../../../utils/pmLookUpId"; import { showToast } from "../../../utils/showToast"; import { formatNumberCentsToAmount } from "../../../utils/stringBuilder"; import { openWebUrl } from "../../../utils/url"; +import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay"; // temporary feature flag since this feature is still WIP // (missing task to complete https://pagopa.atlassian.net/browse/IA-684?filter=10121) @@ -104,15 +106,10 @@ export type ConfirmPaymentMethodScreenNavigationParams = Readonly<{ psps: ReadonlyArray; }>; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "PAYMENT_CONFIRM_PAYMENT_METHOD" ->; - type Props = ReturnType & - ReturnType & - LightModalContextInterface & - OwnProps; + ReturnType; + +type ConfirmPaymentMethodScreenProps = LightModalContextInterface & Props; const styles = StyleSheet.create({ totalContainer: { @@ -225,7 +222,18 @@ const getPaymentMethodType = ( } }; -const ConfirmPaymentMethodScreen: React.FC = (props: Props) => { +const ConfirmPaymentMethodScreen: React.FC = ( + props: ConfirmPaymentMethodScreenProps +) => { + const navigation = useIONavigation(); + const { rptId, initialAmount, verifica, idPayment, wallet, psps } = + useRoute< + Route< + "PAYMENT_CONFIRM_PAYMENT_METHOD", + ConfirmPaymentMethodScreenNavigationParams + > + >().params; + React.useEffect(() => { // show a toast if we got an error while retrieving pm session token if (O.isSome(props.retrievingSessionTokenError)) { @@ -233,13 +241,28 @@ const ConfirmPaymentMethodScreen: React.FC = (props: Props) => { } }, [props.retrievingSessionTokenError]); + const pickPaymentMethod = () => + navigateToPaymentPickPaymentMethodScreen({ + rptId, + initialAmount, + verifica, + idPayment + }); + const pickPsp = () => + navigateToPaymentPickPspScreen({ + rptId, + initialAmount, + verifica, + idPayment, + psps, + wallet, + chooseToChange: true + }); + const urlPrefix = props.isPagoPATestEnabled ? pagoPaApiUrlPrefixTest : pagoPaApiUrlPrefix; - const verifica: PaymentRequestsGetResponse = props.route.params.verifica; - const wallet: Wallet = props.route.params.wallet; - const idPayment: string = props.route.params.idPayment; const paymentReason = verifica.causaleVersamento; const maybePsp = O.fromNullable(wallet.psp); const isPayingWithPaypal = isRawPayPal(wallet.paymentMethod); @@ -271,7 +294,7 @@ const ConfirmPaymentMethodScreen: React.FC = (props: Props) => { ) ) { // store the rptid of a payment done - props.dispatchPaymentCompleteSuccessfully(props.route.params.rptId); + props.dispatchPaymentCompleteSuccessfully(rptId); // refresh transactions list props.loadTransactions(); } else { @@ -304,17 +327,15 @@ const ConfirmPaymentMethodScreen: React.FC = (props: Props) => { // navigate to the screen where the user can pick the desired psp const handleOnEditPaypalPsp = () => { - props.navigation.navigate(ROUTES.WALLET_PAYPAL_UPDATE_PAYMENT_PSP, { - idWallet: wallet.idWallet, - idPayment + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.WALLET_PAYPAL_UPDATE_PAYMENT_PSP, + params: { idWallet: wallet.idWallet, idPayment } }); }; // Handle the PSP change, this will trigger // a different callback for a payment with PayPal. - const handleChangePsp = isPayingWithPaypal - ? handleOnEditPaypalPsp - : props.pickPsp; + const handleChangePsp = isPayingWithPaypal ? handleOnEditPaypalPsp : pickPsp; const formData = pipe( props.payStartWebviewPayload, @@ -374,167 +395,171 @@ const ConfirmPaymentMethodScreen: React.FC = (props: Props) => { ); return ( - - - - - - - -

{I18n.t("wallet.ConfirmPayment.total")}

-

{formattedTotal}

-
- - - - - - -

- {I18n.t("wallet.ConfirmPayment.paymentInformations")} -

-
- - - - -

- {paymentReason} -

- - - {formattedSingleAmount} - -
- - - - - - -

- {I18n.t("wallet.ConfirmPayment.payWith")} -

-
- - - - - - - - - - -

- {I18n.t("wallet.ConfirmPayment.transactionCosts")} -

-
- - - - + + + + + + + +

{I18n.t("wallet.ConfirmPayment.total")}

+

{formattedTotal}

+
+ + + + + + +

+ {I18n.t("wallet.ConfirmPayment.paymentInformations")} +

+
+ + + + +

+ {paymentReason} +

+ + + {formattedSingleAmount} + +
+ + + + + + +

+ {I18n.t("wallet.ConfirmPayment.payWith")} +

+
+ + + + + + + + + + +

+ {I18n.t("wallet.ConfirmPayment.transactionCosts")} +

+
+ + + + + + {isPayingWithPaypal && privacyUrl && ( + <> + + + openWebUrl(privacyUrl)} + accessibilityRole="link" + > + + {`${I18n.t( + "wallet.onboarding.paypal.paymentCheckout.privacyDisclaimer" + )} `} + + + + {I18n.t( + "wallet.onboarding.paypal.paymentCheckout.privacyTerms" + )} + + + + )} + + +
+
+ + {O.isSome(props.payStartWebviewPayload) && ( + + )} - {isPayingWithPaypal && privacyUrl && ( - <> - - - openWebUrl(privacyUrl)} - accessibilityRole="link" - > - - {`${I18n.t( - "wallet.onboarding.paypal.paymentCheckout.privacyDisclaimer" - )} `} - - - - {I18n.t( - "wallet.onboarding.paypal.paymentCheckout.privacyTerms" - )} - - - + + props.dispatchPaymentStart({ + idWallet: wallet.idWallet, + idPayment, + language: getLocalePrimaryWithFallback() + }), + `${I18n.t("wallet.ConfirmPayment.pay")} ${formattedTotal}`, + undefined, + undefined, + O.isSome(props.payStartWebviewPayload) )} - - -
-
- - {O.isSome(props.payStartWebviewPayload) && ( - - )} - - - props.dispatchPaymentStart({ - idWallet: wallet.idWallet, - idPayment, - language: getLocalePrimaryWithFallback() - }), - `${I18n.t("wallet.ConfirmPayment.pay")} ${formattedTotal}`, - undefined, - undefined, - O.isSome(props.payStartWebviewPayload) - )} - /> -
-
+ +
+ ); }; const mapStateToProps = (state: GlobalState) => { @@ -564,29 +589,12 @@ const mapStateToProps = (state: GlobalState) => { }; }; -const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => { +const mapDispatchToProps = (dispatch: Dispatch) => { const dispatchCancelPayment = () => { dispatch(abortRunningPayment()); showToast(I18n.t("wallet.ConfirmPayment.cancelPaymentSuccess"), "success"); }; return { - pickPaymentMethod: () => - navigateToPaymentPickPaymentMethodScreen({ - rptId: props.route.params.rptId, - initialAmount: props.route.params.initialAmount, - verifica: props.route.params.verifica, - idPayment: props.route.params.idPayment - }), - pickPsp: () => - navigateToPaymentPickPspScreen({ - rptId: props.route.params.rptId, - initialAmount: props.route.params.initialAmount, - verifica: props.route.params.verifica, - idPayment: props.route.params.idPayment, - psps: props.route.params.psps, - wallet: props.route.params.wallet, - chooseToChange: true - }), onCancel: () => { ActionSheet.show( { @@ -640,7 +648,12 @@ const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => { }; }; +const ConfirmPaymentMethodScreenWithContext = (props: Props) => { + const { ...modalContext } = React.useContext(LightModalContext); + return ; +}; + export default connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(withLoadingSpinner(ConfirmPaymentMethodScreen))); +)(ConfirmPaymentMethodScreenWithContext); diff --git a/ts/screens/wallet/payment/ManualDataInsertionScreen.tsx b/ts/screens/wallet/payment/ManualDataInsertionScreen.tsx index 4fbf8e07b65..0b8be363e8b 100644 --- a/ts/screens/wallet/payment/ManualDataInsertionScreen.tsx +++ b/ts/screens/wallet/payment/ManualDataInsertionScreen.tsx @@ -18,18 +18,17 @@ import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; import { H1 } from "../../../components/core/typography/H1"; import { Link } from "../../../components/core/typography/Link"; import { IOStyles } from "../../../components/core/variables/IOStyles"; - -import { withLightModalContext } from "../../../components/helpers/withLightModalContext"; import { LabelledItem } from "../../../components/LabelledItem"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import { LightModalContextInterface } from "../../../components/ui/LightModal"; +import { + LightModalContext, + LightModalContextInterface +} from "../../../components/ui/LightModal"; import { cancelButtonProps } from "../../../components/buttons/ButtonConfigurations"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; import { navigateBack, navigateToPaymentTransactionSummaryScreen, @@ -48,15 +47,10 @@ export type ManualDataInsertionScreenNavigationParams = { isInvalidAmount?: boolean; }; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "PAYMENT_MANUAL_DATA_INSERTION" ->; +type Props = ReturnType & + ReturnType; -type Props = OwnProps & - ReturnType & - ReturnType & - LightModalContextInterface; +type ManualDataInsertionScreenProps = Props & LightModalContextInterface; type State = Readonly<{ paymentNoticeNumber: O.Option< @@ -93,8 +87,11 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * - integrate contextual help to obtain details on the data to insert for manually identifying the transaction * https://www.pivotaltracker.com/n/projects/2048617/stories/157874540 */ -class ManualDataInsertionScreen extends React.Component { - constructor(props: Props) { +class ManualDataInsertionScreen extends React.Component< + ManualDataInsertionScreenProps, + State +> { + constructor(props: ManualDataInsertionScreenProps) { super(props); this.state = { paymentNoticeNumber: O.none, @@ -317,7 +314,12 @@ const mapStateToProps = (state: GlobalState) => ({ hasMethodsCanPay: withPaymentFeatureSelector(state).length > 0 }); +const ManualDataInsertionScreenFC = (props: Props) => { + const { ...modalContext } = React.useContext(LightModalContext); + return ; +}; + export default connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(ManualDataInsertionScreen)); +)(ManualDataInsertionScreenFC); diff --git a/ts/screens/wallet/payment/PaymentOutcomeCodeMessage.tsx b/ts/screens/wallet/payment/PaymentOutcomeCodeMessage.tsx index 3cf68bc7b43..a7c789e15c5 100644 --- a/ts/screens/wallet/payment/PaymentOutcomeCodeMessage.tsx +++ b/ts/screens/wallet/payment/PaymentOutcomeCodeMessage.tsx @@ -6,6 +6,7 @@ import { View } from "react-native"; import { widthPercentageToDP } from "react-native-responsive-screen"; import { connect } from "react-redux"; import { Dispatch } from "redux"; +import { Route, useRoute } from "@react-navigation/native"; import { ImportoEuroCents } from "../../../../definitions/backend/ImportoEuroCents"; import paymentCompleted from "../../../../img/pictograms/payment-completed.png"; import { cancelButtonProps } from "../../../components/buttons/ButtonConfigurations"; @@ -14,11 +15,9 @@ import { InfoScreenComponent } from "../../../components/infoScreen/InfoScreenCo import { renderInfoRasterImage } from "../../../components/infoScreen/imageRendering"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; import OutcomeCodeMessageComponent from "../../../components/wallet/OutcomeCodeMessageComponent"; -import { WalletPaymentFeebackBanner } from "../../../features/walletV3/payment/components/WalletPaymentFeedbackBanner"; +import { WalletPaymentFeebackBanner } from "../../../features/payments/checkout/components/WalletPaymentFeedbackBanner"; import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; import { navigateToWalletHome } from "../../../store/actions/navigation"; import { backToEntrypointPayment } from "../../../store/actions/wallet/payment"; import { profileEmailSelector } from "../../../store/reducers/profile"; @@ -35,14 +34,8 @@ export type PaymentOutcomeCodeMessageNavigationParams = Readonly<{ fee: ImportoEuroCents; }>; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "PAYMENT_OUTCOMECODE_MESSAGE" ->; - type Props = ReturnType & - ReturnType & - OwnProps; + ReturnType; const SuccessBody = ({ emailAddress }: { emailAddress: string }) => ( @@ -101,6 +94,13 @@ const successFooter = (onClose: () => void) => ( * If the outcome code is of type success the render a single buttons footer that allow the user to go to the wallet home. */ const PaymentOutcomeCodeMessage: React.FC = (props: Props) => { + const { fee } = + useRoute< + Route< + "PAYMENT_OUTCOMECODE_MESSAGE", + PaymentOutcomeCodeMessageNavigationParams + > + >().params; const outcomeCode = O.toNullable(props.outcomeCode.outcomeCode); const learnMoreLink = "https://io.italia.it/faq/#pagamenti"; const onLearnMore = () => openWebUrl(learnMoreLink); @@ -114,7 +114,7 @@ const PaymentOutcomeCodeMessage: React.FC = (props: Props) => { if (pot.isSome(props.verifica)) { const totalAmount = (props.verifica.value.importoSingoloVersamento as number) + - (props.route.params.fee as number); + (fee as number); return successComponent( O.getOrElse(() => "")(props.profileEmail), diff --git a/ts/screens/wallet/payment/PickPaymentMethodScreen.tsx b/ts/screens/wallet/payment/PickPaymentMethodScreen.tsx index 03e68268c12..ab3436b6a34 100644 --- a/ts/screens/wallet/payment/PickPaymentMethodScreen.tsx +++ b/ts/screens/wallet/payment/PickPaymentMethodScreen.tsx @@ -10,6 +10,7 @@ import { FlatList, SafeAreaView } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; import { connect } from "react-redux"; import { VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useNavigation, useRoute } from "@react-navigation/native"; import { PaymentRequestsGetResponse } from "../../../../definitions/backend/PaymentRequestsGetResponse"; import { withLoadingSpinner } from "../../../components/helpers/withLoadingSpinner"; import BaseScreenComponent, { @@ -32,7 +33,10 @@ import { } from "../../../common/model/RemoteValue"; import PaymentStatusSwitch from "../../../features/wallet/component/features/PaymentStatusSwitch"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { + IOStackNavigationProp, + IOStackNavigationRouteProps +} from "../../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; import { navigateBack, @@ -282,7 +286,25 @@ const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => ({ }) }); -export default connect( +const ConnectedPickPaymentMethodScreen = connect( mapStateToProps, mapDispatchToProps )(withLoadingSpinner(PickPaymentMethodScreen)); + +const PickPaymentMethodScreenFC = () => { + const navigation = + useNavigation< + IOStackNavigationProp + >(); + const route = + useRoute< + Route< + "PAYMENT_PICK_PAYMENT_METHOD", + PickPaymentMethodScreenNavigationParams + > + >(); + return ( + + ); +}; +export default PickPaymentMethodScreenFC; diff --git a/ts/screens/wallet/payment/PickPspScreen.tsx b/ts/screens/wallet/payment/PickPspScreen.tsx index 77554577f80..edfd5344335 100644 --- a/ts/screens/wallet/payment/PickPspScreen.tsx +++ b/ts/screens/wallet/payment/PickPspScreen.tsx @@ -3,21 +3,23 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import * as React from "react"; import { View, FlatList, SafeAreaView, StyleSheet } from "react-native"; import { connect } from "react-redux"; - import { VSpacer } from "@pagopa/io-app-design-system"; +import { Route, useNavigation, useRoute } from "@react-navigation/native"; import { PaymentRequestsGetResponse } from "../../../../definitions/backend/PaymentRequestsGetResponse"; import { PspData } from "../../../../definitions/pagopa/PspData"; import { H1 } from "../../../components/core/typography/H1"; import { H4 } from "../../../components/core/typography/H4"; import { H5 } from "../../../components/core/typography/H5"; import { IOStyles } from "../../../components/core/variables/IOStyles"; -import { withLightModalContext } from "../../../components/helpers/withLightModalContext"; import ItemSeparatorComponent from "../../../components/ItemSeparatorComponent"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import { LightModalContextInterface } from "../../../components/ui/LightModal"; +import { + LightModalContext, + LightModalContextInterface +} from "../../../components/ui/LightModal"; import { PspComponent } from "../../../components/wallet/payment/PspComponent"; import { cancelButtonProps } from "../../../components/buttons/ButtonConfigurations"; import { LoadingErrorComponent } from "../../../components/LoadingErrorComponent"; @@ -27,7 +29,10 @@ import { isLoading } from "../../../common/model/RemoteValue"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { + IOStackNavigationProp, + IOStackNavigationRouteProps +} from "../../../navigation/params/AppParamsList"; import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; import { navigateBack } from "../../../store/actions/navigation"; import { Dispatch } from "../../../store/actions/types"; @@ -56,9 +61,9 @@ type OwnProps = IOStackNavigationRouteProps< >; type Props = ReturnType & - ReturnType & - LightModalContextInterface & - OwnProps; + ReturnType; + +type PickPspScreenProps = LightModalContextInterface & Props & OwnProps; const styles = StyleSheet.create({ header: { @@ -77,7 +82,7 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { /** * Select a PSP to be used for a the current selected wallet */ -class PickPspScreen extends React.Component { +class PickPspScreen extends React.Component { public componentDidMount() { // load all psp in order to offer to the user the complete psps list const idWallet = this.props.route.params.wallet.idWallet; @@ -201,7 +206,26 @@ const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => ({ ) }); -export default connect( +const ConnectedPickPspScreen = connect( mapStateToProps, mapDispatchToProps -)(withLightModalContext(PickPspScreen)); +)(PickPspScreen); + +const PickPspScreenFC = () => { + const { ...modalContext } = React.useContext(LightModalContext); + const navigation = + useNavigation< + IOStackNavigationProp + >(); + const route = + useRoute>(); + return ( + + ); +}; + +export default PickPspScreenFC; diff --git a/ts/screens/wallet/payment/TransactionErrorScreen.tsx b/ts/screens/wallet/payment/TransactionErrorScreen.tsx index 78048ae8ef0..51cfc4d1730 100644 --- a/ts/screens/wallet/payment/TransactionErrorScreen.tsx +++ b/ts/screens/wallet/payment/TransactionErrorScreen.tsx @@ -12,6 +12,7 @@ import { ComponentProps } from "react"; import { View, SafeAreaView } from "react-native"; import { connect } from "react-redux"; import { VSpacer, IOPictograms } from "@pagopa/io-app-design-system"; +import { Route, useRoute } from "@react-navigation/native"; import { Detail_v2Enum } from "../../../../definitions/backend/PaymentProblemJson"; import { ToolEnum } from "../../../../definitions/content/AssistanceToolConfig"; import { ZendeskCategory } from "../../../../definitions/content/ZendeskCategory"; @@ -30,8 +31,6 @@ import { } from "../../../features/zendesk/store/actions"; import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton"; import I18n from "../../../i18n"; -import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; import { navigateToPaymentManualDataInsertion } from "../../../store/actions/navigation"; import { Dispatch } from "../../../store/actions/types"; import { @@ -80,13 +79,7 @@ export type TransactionErrorScreenNavigationParams = { onCancel: () => void; }; -type OwnProps = IOStackNavigationRouteProps< - WalletParamsList, - "PAYMENT_TRANSACTION_ERROR" ->; - -type Props = OwnProps & - ReturnType & +type Props = ReturnType & ReturnType; const imageTimeout: IOPictograms = "inProgress"; @@ -357,9 +350,11 @@ export const errorTransactionUIElements = ( }; const TransactionErrorScreen = (props: Props) => { - const rptId = props.route.params.rptId; - const error = props.route.params.error; - const onCancel = props.route.params.onCancel; + const { rptId, error, onCancel } = + useRoute< + Route<"PAYMENT_TRANSACTION_ERROR", TransactionErrorScreenNavigationParams> + >().params; + const { paymentsHistory } = props; const codiceAvviso = getCodiceAvviso(rptId); diff --git a/ts/screens/wallet/payment/TransactionSummaryScreen.tsx b/ts/screens/wallet/payment/TransactionSummaryScreen.tsx index e0c0952019a..7e23f2635ea 100644 --- a/ts/screens/wallet/payment/TransactionSummaryScreen.tsx +++ b/ts/screens/wallet/payment/TransactionSummaryScreen.tsx @@ -1,6 +1,7 @@ import { ButtonSolid, ContentWrapper, + IOToast, VSpacer } from "@pagopa/io-app-design-system"; import { @@ -17,26 +18,20 @@ import React, { useCallback, useEffect } from "react"; import { ScrollView, StyleSheet } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useDispatch } from "react-redux"; -import { PaymentRequestsGetResponse } from "../../../../definitions/backend/PaymentRequestsGetResponse"; -import { IOToast } from "../../../components/Toast"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import { isError as isRemoteError, isLoading as isRemoteLoading, isUndefined } from "../../../common/model/RemoteValue"; +import { IOStyles } from "../../../components/core/variables/IOStyles"; +import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import { zendeskSelectedCategory, zendeskSupportStart } from "../../../features/zendesk/store/actions"; import I18n from "../../../i18n"; import { WalletParamsList } from "../../../navigation/params/WalletParamsList"; -import { - navigateToPaymentPickPaymentMethodScreen, - navigateToPaymentTransactionErrorScreen, - navigateToWalletAddPaymentMethod -} from "../../../store/actions/navigation"; +import { navigateToPaymentTransactionErrorScreen } from "../../../store/actions/navigation"; import { PaymentStartOrigin, abortRunningPayment, @@ -46,8 +41,7 @@ import { paymentIdPolling, paymentInitializeState, paymentVerifica, - runDeleteActivePaymentSaga, - runStartOrResumePaymentActivationSaga + runDeleteActivePaymentSaga } from "../../../store/actions/wallet/payment"; import { fetchWalletsRequestWithExpBackoff } from "../../../store/actions/wallet/wallets"; import { useIOSelector } from "../../../store/hooks"; @@ -55,10 +49,7 @@ import { bancomatPayConfigSelector, isPaypalEnabledSelector } from "../../../store/reducers/backendStatus"; -import { - getFavoriteWallet, - withPaymentFeatureSelector -} from "../../../store/reducers/wallet/wallets"; +import { getFavoriteWallet } from "../../../store/reducers/wallet/wallets"; import customVariables from "../../../theme/variables"; import { PayloadForAction } from "../../../types/utils"; import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; @@ -69,7 +60,6 @@ import { getV2ErrorMainType, isDuplicatedPayment } from "../../../utils/payment"; -import { alertNoPayablePaymentMethods } from "../../../utils/paymentMethod"; import { addTicketCustomField, appendLog, @@ -81,10 +71,10 @@ import { zendeskPaymentOrgFiscalCode, zendeskPaymentStartOrigin } from "../../../utils/supportAssistance"; -import { dispatchPickPspOrConfirm } from "./common"; import { TransactionSummary } from "./components/TransactionSummary"; import { TransactionSummaryErrorDetails } from "./components/TransactionSummaryErrorDetails"; import { TransactionSummaryStatus } from "./components/TransactionSummaryStatus"; +import { useStartOrResumePayment } from "./hooks/useStartOrResumePayment"; export type TransactionSummaryScreenNavigationParams = Readonly<{ rptId: RptId; @@ -214,9 +204,6 @@ const TransactionSummaryScreen = (): React.ReactElement => { }) ); - const hasPayableMethods = - useIOSelector(withPaymentFeatureSelector).length > 0; - const isLoading = pot.isLoading(walletById) || pot.isLoading(paymentVerification) || @@ -335,55 +322,13 @@ const TransactionSummaryScreen = (): React.ReactElement => { paymentVerifica.request({ rptId, startOrigin: paymentStartOrigin }) ); - const startOrResumePayment = useCallback( - (paymentVerification: PaymentRequestsGetResponse) => - dispatch( - runStartOrResumePaymentActivationSaga({ - rptId, - verifica: paymentVerification, - onSuccess: idPayment => - dispatchPickPspOrConfirm(dispatch)( - rptId, - initialAmount, - paymentVerification, - idPayment, - maybeFavoriteWallet, - () => { - // either we cannot use the default payment method for this - // payment, or fetching the PSPs for this payment and the - // default wallet has failed, ask the user to pick a wallet - - navigateToPaymentPickPaymentMethodScreen({ - rptId, - initialAmount, - verifica: paymentVerification, - idPayment - }); - }, - hasPayableMethods - ) - }) - ), - [dispatch, hasPayableMethods, initialAmount, maybeFavoriteWallet, rptId] + const continueWithPayment = useStartOrResumePayment( + rptId, + pot.toOption(paymentVerification), + initialAmount, + maybeFavoriteWallet ); - const continueWithPayment = useCallback(() => { - if (!pot.isSome(paymentVerification)) { - return; - } - if (hasPayableMethods) { - startOrResumePayment(paymentVerification.value); - return; - } - - alertNoPayablePaymentMethods(() => - navigateToWalletAddPaymentMethod({ - inPayment: O.none, - showOnlyPayablePaymentMethods: true - }) - ); - }, [hasPayableMethods, startOrResumePayment, paymentVerification]); - const resetPayment = () => { dispatch(runDeleteActivePaymentSaga()); dispatch(paymentInitializeState()); diff --git a/ts/screens/wallet/payment/components/TransactionSummary.tsx b/ts/screens/wallet/payment/components/TransactionSummary.tsx index c34b96d7b08..de816b5c5b8 100644 --- a/ts/screens/wallet/payment/components/TransactionSummary.tsx +++ b/ts/screens/wallet/payment/components/TransactionSummary.tsx @@ -118,7 +118,11 @@ export const TransactionSummary = (props: Props): React.ReactElement => { const amount = pot.toUndefined( pot.map(props.paymentVerification, _ => - formatNumberAmount(centsToAmount(_.importoSingoloVersamento), true) + formatNumberAmount( + centsToAmount(_.importoSingoloVersamento), + true, + "right" + ) ) ); diff --git a/ts/screens/wallet/payment/hooks/useStartOrResumePayment.ts b/ts/screens/wallet/payment/hooks/useStartOrResumePayment.ts new file mode 100644 index 00000000000..6f67538d51f --- /dev/null +++ b/ts/screens/wallet/payment/hooks/useStartOrResumePayment.ts @@ -0,0 +1,131 @@ +import { AmountInEuroCents, RptId } from "@pagopa/io-pagopa-commons/lib/pagopa"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import React from "react"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse"; +import { isUndefined } from "../../../../common/model/RemoteValue"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../navigation/routes"; +import { + paymentAttiva, + paymentCheck, + paymentIdPolling +} from "../../../../store/actions/wallet/payment"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { withPaymentFeatureSelector } from "../../../../store/reducers/wallet/wallets"; +import { Wallet } from "../../../../types/pagopa"; +import { alertNoPayablePaymentMethods } from "../../../../utils/paymentMethod"; +import { dispatchPickPspOrConfirm } from "../common"; + +const useStartOrResumePayment = ( + rptId: RptId, + paymentVerification: O.Option, + initialAmount: AmountInEuroCents, + maybeSelectedWallet: O.Option +) => { + const navigation = useIONavigation(); + const dispatch = useIODispatch(); + + const hasPayableMethods = + useIOSelector(withPaymentFeatureSelector).length > 0; + + const paymentAttivaPot = useIOSelector(_ => _.wallet.payment.attiva); + const paymentIdPot = useIOSelector(_ => _.wallet.payment.paymentId); + const paymentCheckPot = useIOSelector(_ => _.wallet.payment.check); + const pspsRemoteValue = useIOSelector(_ => _.wallet.payment.pspsV2.psps); + + React.useEffect(() => { + pipe( + paymentVerification, + O.map(verifica => { + if (pot.isNone(paymentAttivaPot)) { + // If payment activation has not yet been requested, skip + return; + } + + if (pot.isNone(paymentIdPot) && !pot.isLoading(paymentIdPot)) { + // Poll for payment ID + dispatch(paymentIdPolling.request(verifica)); + } + + if (pot.isSome(paymentIdPot)) { + const idPayment = paymentIdPot.value; + + // "check" the payment + if (pot.isNone(paymentCheckPot) && !pot.isLoading(paymentCheckPot)) { + dispatch(paymentCheck.request(idPayment)); + } + + if (pot.isSome(paymentCheckPot) && isUndefined(pspsRemoteValue)) { + // Navigate to method or PSP selection screen + dispatchPickPspOrConfirm(dispatch)( + rptId, + initialAmount, + verifica, + idPayment, + maybeSelectedWallet, + () => { + // either we cannot use the default payment method for this + // payment, or fetching the PSPs for this payment and the + // default wallet has failed, ask the user to pick a wallet + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.PAYMENT_PICK_PAYMENT_METHOD, + params: { + rptId, + initialAmount, + verifica, + idPayment + } + }); + }, + hasPayableMethods + ); + } + } + }) + ); + }, [ + dispatch, + navigation, + paymentVerification, + paymentAttivaPot, + paymentIdPot, + paymentCheckPot, + hasPayableMethods, + pspsRemoteValue, + rptId, + initialAmount, + maybeSelectedWallet + ]); + + return React.useCallback(() => { + if (!hasPayableMethods) { + alertNoPayablePaymentMethods(() => + navigation.navigate(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.WALLET_ADD_PAYMENT_METHOD, + params: { + inPayment: O.none, + showOnlyPayablePaymentMethods: true + } + }) + ); + + return; + } + + pipe( + paymentVerification, + O.map(verifica => + dispatch( + paymentAttiva.request({ + rptId, + verifica + }) + ) + ) + ); + }, [dispatch, navigation, hasPayableMethods, rptId, paymentVerification]); +}; + +export { useStartOrResumePayment }; diff --git a/ts/store/actions/identification.ts b/ts/store/actions/identification.ts index ef7ee6250f6..de1c66eb866 100644 --- a/ts/store/actions/identification.ts +++ b/ts/store/actions/identification.ts @@ -1,4 +1,8 @@ -import { ActionType, createAction } from "typesafe-actions"; +import { + ActionType, + createAction, + createStandardAction +} from "typesafe-actions"; import { PinString } from "../../types/PinString"; import { @@ -59,13 +63,18 @@ export const identificationStart = createAction( ); export const identificationCancel = createAction("IDENTIFICATION_CANCEL"); -export const identificationSuccess = createAction("IDENTIFICATION_SUCCESS"); +export const identificationSuccess = createStandardAction( + "IDENTIFICATION_SUCCESS" +)<{ isBiometric: boolean }>(); export const identificationFailure = createAction("IDENTIFICATION_FAILURE"); export const identificationPinReset = createAction("IDENTIFICATION_PIN_RESET"); export const identificationReset = createAction("IDENTIFICATION_RESET"); export const identificationForceLogout = createAction( "IDENTIFICATION_FORCE_LOGOUT" ); +export const identificationHideLockModal = createAction( + "IDENTIFICATION_HIDE_LOCK_MODAL" +); export type IdentificationActions = | ActionType @@ -75,4 +84,5 @@ export type IdentificationActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType; diff --git a/ts/store/actions/navigation.ts b/ts/store/actions/navigation.ts index fde450c7bb1..5964dfb6cb1 100644 --- a/ts/store/actions/navigation.ts +++ b/ts/store/actions/navigation.ts @@ -4,7 +4,6 @@ import NavigationService from "../../navigation/NavigationService"; import ROUTES from "../../navigation/routes"; import { CieCardReaderScreenNavigationParams } from "../../screens/authentication/cie/CieCardReaderScreen"; import { OnboardingServicesPreferenceScreenNavigationParams } from "../../screens/onboarding/OnboardingServicesPreferenceScreen"; -import { ServiceDetailsScreenNavigationParams } from "../../screens/services/ServiceDetailsScreen"; import { AddCardScreenNavigationParams } from "../../screens/wallet/AddCardScreen"; import { AddCreditCardOutcomeCodeMessageNavigationParams } from "../../screens/wallet/AddCreditCardOutcomeCodeMessage"; import { AddPaymentMethodScreenNavigationParams } from "../../screens/wallet/AddPaymentMethodScreen"; @@ -25,6 +24,8 @@ import { BPayPaymentMethod, CreditCardPaymentMethod } from "../../types/pagopa"; +import { SERVICES_ROUTES } from "../../features/services/navigation/routes"; +import { ServiceDetailsScreenNavigationParams } from "../../features/services/screens/ServiceDetailsScreen"; /** * @deprecated @@ -111,28 +112,6 @@ export const navigateToServicesPreferenceModeSelectionScreen = ( }) ); -/** - * Email - */ - -/** - * @deprecated - */ -export const navigateToEmailReadScreen = () => { - NavigationService.dispatchNavigationAction( - CommonActions.navigate(ROUTES.READ_EMAIL_SCREEN) - ); -}; - -/** - * @deprecated - */ -export const navigateToEmailInsertScreen = () => { - NavigationService.dispatchNavigationAction( - CommonActions.navigate(ROUTES.INSERT_EMAIL_SCREEN) - ); -}; - /** * Service */ @@ -142,8 +121,8 @@ export const navigateToEmailInsertScreen = () => { */ export const navigateToServiceHomeScreen = () => NavigationService.dispatchNavigationAction( - CommonActions.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICES_HOME + CommonActions.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICES_HOME }) ); @@ -154,8 +133,8 @@ export const navigateToServiceDetailsScreen = ( params: ServiceDetailsScreenNavigationParams ) => NavigationService.dispatchNavigationAction( - CommonActions.navigate(ROUTES.SERVICES_NAVIGATOR, { - screen: ROUTES.SERVICE_DETAIL, + CommonActions.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { + screen: SERVICES_ROUTES.SERVICE_DETAIL, params }) ); diff --git a/ts/store/actions/persistedPreferences.ts b/ts/store/actions/persistedPreferences.ts index e75ebd10316..e8947101fee 100644 --- a/ts/store/actions/persistedPreferences.ts +++ b/ts/store/actions/persistedPreferences.ts @@ -49,6 +49,14 @@ export const preferencesDesignSystemSetEnabled = createStandardAction( "PREFERENCES_DESIGN_SYSTEM_SET_ENABLED" )<{ isDesignSystemEnabled: boolean }>(); +export const preferencesNewWalletSectionSetEnabled = createStandardAction( + "PREFERENCES_NEW_WALLET_SECTION_SET_ENABLED" +)<{ isNewWalletSectionEnabled: boolean }>(); + +export const preferencesItWalletTestSetEnabled = createStandardAction( + "PREFERENCES_ITWALLET_TEST_SET_ENABLED" +)<{ isItWalletTestEnabled: boolean }>(); + export type PersistedPreferencesActions = ActionType< // eslint-disable-next-line | typeof preferenceFingerprintIsEnabledSaveSuccess @@ -62,4 +70,6 @@ export type PersistedPreferencesActions = ActionType< | typeof preferencesPnTestEnvironmentSetEnabled | typeof preferencesIdPayTestSetEnabled | typeof preferencesDesignSystemSetEnabled + | typeof preferencesNewWalletSectionSetEnabled + | typeof preferencesItWalletTestSetEnabled >; diff --git a/ts/store/actions/profile.ts b/ts/store/actions/profile.ts index 8b77dcba261..6bd55c72e1f 100644 --- a/ts/store/actions/profile.ts +++ b/ts/store/actions/profile.ts @@ -82,6 +82,13 @@ export const removeAccountMotivation = createStandardAction( "REMOVE_ACCOUNT_MOTIVATION" )(); +export const emailValidationPollingStart = createAction( + "EMAIL_VALIDATION_POLLING_START" +); +export const emailValidationPollingStop = createAction( + "EMAIL_VALIDATION_POLLING_STOP" +); + export type ProfileActions = | ActionType | ActionType @@ -93,4 +100,6 @@ export type ProfileActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType + | ActionType; diff --git a/ts/store/actions/services/index.ts b/ts/store/actions/services/index.ts index 9baf2bd9ae2..763a286f8e0 100644 --- a/ts/store/actions/services/index.ts +++ b/ts/store/actions/services/index.ts @@ -15,7 +15,7 @@ import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; import { loadServicePreference, upsertServicePreference -} from "./servicePreference"; +} from "../../../features/services/store/actions"; // // service loading at startup diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index e64628eb068..6988c953845 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -16,12 +16,13 @@ import { FciActions } from "../../features/fci/store/actions"; import { IdPayActions } from "../../features/idpay/common/store/actions"; import { LollipopActions } from "../../features/lollipop/store/actions/lollipop"; import { MessagesActions } from "../../features/messages/store/actions"; +import { WalletActions as NewWalletActions } from "../../features/newWallet/store/actions"; +import { PaymentsActions as PaymentsFeatureActions } from "../../features/payments/common/store/actions"; import { PnActions } from "../../features/pn/store/actions"; import { AbiActions } from "../../features/wallet/onboarding/bancomat/store/actions"; import { BPayActions } from "../../features/wallet/onboarding/bancomatPay/store/actions"; import { CoBadgeActions } from "../../features/wallet/onboarding/cobadge/store/actions"; import { PayPalOnboardingActions } from "../../features/wallet/onboarding/paypal/store/actions"; -import { WalletActions as WalletV3Actions } from "../../features/walletV3/common/store/actions"; import { WhatsNewActions } from "../../features/whatsnew/store/actions"; import { ZendeskSupportActions } from "../../features/zendesk/store/actions"; import { GlobalState } from "../reducers/types"; @@ -98,7 +99,8 @@ export type Action = | LollipopActions | FastLoginActions | WhatsNewActions - | WalletV3Actions; + | PaymentsFeatureActions + | NewWalletActions; export type Dispatch = DispatchAPI; diff --git a/ts/store/actions/wallet/payment.ts b/ts/store/actions/wallet/payment.ts index c878b6fb94e..e8aecf8ddfd 100644 --- a/ts/store/actions/wallet/payment.ts +++ b/ts/store/actions/wallet/payment.ts @@ -239,20 +239,6 @@ export const abortRunningPayment = createStandardAction( "PAYMENT_ABORT_RUNNING_PAYMENT" )(); -// -// run startOrResumePaymentSaga -// - -type RunStartOrResumePaymentActivationSagaPayload = Readonly<{ - rptId: RptId; - verifica: PaymentRequestsGetResponse; - onSuccess: (idPayment: string) => void; -}>; - -export const runStartOrResumePaymentActivationSaga = createStandardAction( - "PAYMENT_RUN_START_OR_RESUME_PAYMENT_ACTIVATION_SAGA" -)(); - /** * the psp selected for the payment */ @@ -287,6 +273,12 @@ export const pspForPaymentV2WithCallbacks = createStandardAction( onFailure: () => void; }>(); +// This action is used to notify that wallet sagas handlers have been initialized +// Used by the Fast Login sagas to wait before dispatching any pending actions +export const walletPaymentHandlersInitialized = createStandardAction( + "WALLET_PAYMENT_HANDLERS_INITIALIZED" +)(); + /** * All possible payment actions */ @@ -307,7 +299,7 @@ export type PaymentActions = | ActionType | ActionType | ActionType - | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType; diff --git a/ts/store/hooks.ts b/ts/store/hooks.ts index cf9150bee82..b42785341b1 100644 --- a/ts/store/hooks.ts +++ b/ts/store/hooks.ts @@ -1,6 +1,12 @@ -import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import { + TypedUseSelectorHook, + useDispatch, + useSelector, + useStore +} from "react-redux"; import { AppDispatch } from "../App"; import { GlobalState } from "./reducers/types"; export const useIODispatch = () => useDispatch(); export const useIOSelector: TypedUseSelectorHook = useSelector; +export const useIOStore = () => useStore(); diff --git a/ts/store/middlewares/analytics.ts b/ts/store/middlewares/analytics.ts index c8a074a678b..cd5ed8c5807 100644 --- a/ts/store/middlewares/analytics.ts +++ b/ts/store/middlewares/analytics.ts @@ -106,13 +106,14 @@ import { setFavouriteWalletSuccess, updatePaymentStatus } from "../actions/wallet/wallets"; +import { buildEventProperties } from "../../utils/analytics"; import { trackContentAction } from "./contentAnalytics"; import { trackServiceAction } from "./serviceAnalytics"; const trackAction = (mp: NonNullable) => // eslint-disable-next-line complexity - (action: Action): Promise> => { + (action: Action): void | ReadonlyArray => { // eslint-disable-next-line sonarjs/max-switch-cases switch (action.type) { // @@ -183,12 +184,11 @@ const trackAction = // Only in the former case we have a transaction and an amount. if (action.payload.kind === "COMPLETED") { const amount = action.payload.transaction?.amount.amount; - return mp - .track(action.type, { - amount, - kind: action.payload.kind - }) - .then(_ => mp.trackCharge(amount ?? -1)); + mp.track(action.type, { + amount, + kind: action.payload.kind + }); + return mp.getPeople().trackCharge(amount ?? -1, {}); } else { return mp.track(action.type, { kind: action.payload.kind @@ -306,10 +306,11 @@ const trackAction = case getType(sessionInvalid): case getType(logoutSuccess): // identification + // identificationSuccess is handled separately + // because it has a payload. case getType(identificationRequest): case getType(identificationStart): case getType(identificationCancel): - case getType(identificationSuccess): case getType(identificationFailure): case getType(identificationPinReset): case getType(identificationForceLogout): @@ -370,8 +371,15 @@ const trackAction = choice: action.payload.choice, reason: action.payload.error.message }); + // identification: identificationSuccess + case getType(identificationSuccess): + return mp.track( + action.type, + buildEventProperties("UX", "confirm", { + identification_method: action.payload.isBiometric ? "bio" : "pin" + }) + ); } - return Promise.resolve(); }; /* diff --git a/ts/store/middlewares/contentAnalytics.ts b/ts/store/middlewares/contentAnalytics.ts index 61dd25325c3..049bbe5791f 100644 --- a/ts/store/middlewares/contentAnalytics.ts +++ b/ts/store/middlewares/contentAnalytics.ts @@ -5,7 +5,7 @@ import { loadContextualHelpData, loadIdps } from "../actions/content"; export const trackContentAction = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(loadContextualHelpData.request): case getType(loadContextualHelpData.success): @@ -18,5 +18,4 @@ export const trackContentAction = reason: action.payload }); } - return Promise.resolve(); }; diff --git a/ts/store/middlewares/serviceAnalytics.ts b/ts/store/middlewares/serviceAnalytics.ts index 322390199b5..f9178177b1b 100644 --- a/ts/store/middlewares/serviceAnalytics.ts +++ b/ts/store/middlewares/serviceAnalytics.ts @@ -9,13 +9,12 @@ import { import { loadServicePreference, upsertServicePreference -} from "../actions/services/servicePreference"; -import { getNetworkErrorMessage } from "../../utils/errors"; +} from "../../features/services/store/actions"; // Isolated tracker for services actions export const trackServiceAction = (mp: NonNullable) => - (action: Action): Promise => { + (action: Action): void => { switch (action.type) { case getType(loadServicesDetail): return mp.track(action.type, { @@ -33,7 +32,7 @@ export const trackServiceAction = case getType(upsertServicePreference.failure): return mp.track(action.type, { service_id: action.payload.id, - reason: getNetworkErrorMessage(action.payload) + reason: action.payload }); case getType(loadVisibleServices.request): case getType(loadVisibleServices.success): @@ -52,5 +51,4 @@ export const trackServiceAction = responseStatus: action.payload.kind }); } - return Promise.resolve(); }; diff --git a/ts/store/reducers/__tests__/identification.test.ts b/ts/store/reducers/__tests__/identification.test.ts index 53a6d8969ed..0d7a0ea189f 100644 --- a/ts/store/reducers/__tests__/identification.test.ts +++ b/ts/store/reducers/__tests__/identification.test.ts @@ -29,7 +29,10 @@ describe("Identification reducer", () => { }); it("should return correct state after identification success", () => { const startState = reducer(undefined, identificationStartMock); - const successState = reducer(startState, identificationSuccess()); + const successState = reducer( + startState, + identificationSuccess({ isBiometric: false }) + ); expect(successState.progress.kind).toEqual("identified"); expect(successState.fail).toEqual(undefined); }); @@ -85,7 +88,10 @@ describe("Identification reducer", () => { }; // after a success the fail state is cleared - expectFailStateReset(identificationSuccess(), "identified"); + expectFailStateReset( + identificationSuccess({ isBiometric: false }), + "identified" + ); // after a reset the fail state is cleared expectFailStateReset(identificationReset(), "unidentified"); @@ -109,7 +115,7 @@ describe("Identification reducer", () => { // start the full identification sequence from different states [ identificationCancel(), - identificationSuccess(), + identificationSuccess({ isBiometric: false }), identificationStartMock ].forEach(action => expectFailSequenceFromStartingState(action)); }); @@ -119,7 +125,8 @@ describe("Identification reducer", () => { pipe( identificationResetState, expectFailSequence, - (state: IdentificationState) => reducer(state, identificationSuccess()), + (state: IdentificationState) => + reducer(state, identificationSuccess({ isBiometric: false })), expectFailSequence, (state: IdentificationState) => reducer(state, identificationReset()), expectFailSequence diff --git a/ts/store/reducers/__tests__/identificationStateMigration.test.ts b/ts/store/reducers/__tests__/identificationStateMigration.test.ts new file mode 100644 index 00000000000..4208981ecca --- /dev/null +++ b/ts/store/reducers/__tests__/identificationStateMigration.test.ts @@ -0,0 +1,94 @@ +import MockDate from "mockdate"; +import { PersistPartial } from "redux-persist"; +import { + IDENTIFICATION_STATE_MIGRATION_VERSION, + identificationStateMigration +} from ".."; +import { IdentificationState } from "../identification"; + +MockDate.set(new Date("2024-03-13T10:30:20.000Z")); + +const lastAttempt = new Date("2024-03-13T10:30:00.000Z"); + +// "progress" is in black list, so we need to omit it from the presisted state +type MockState = Omit & PersistPartial; +// A mock of the state before the migration +const previousState: MockState = { + _persist: { + version: IDENTIFICATION_STATE_MIGRATION_VERSION - 1, + rehydrated: true + }, + fail: { + nextLegalAttempt: lastAttempt, + remainingAttempts: 7, + timespanBetweenAttempts: 0 + } +}; + +describe("IdentificationStateMigration", () => { + it("should pass the sanity check", () => { + const actualDate = new Date(); + const expectedDate = new Date("2024-03-13T10:30:20.000Z"); + expect(actualDate).toEqual(expectedDate); + }); +}); + +describe("IdentificationStateMigration", () => { + it("should add showLockModal=false to fail object if fail exists and remainingAttempts > 3", () => { + // Execute the migration + const migratedState = identificationStateMigration["0"]( + previousState + ) as MockState; + + // Assertions + expect(migratedState).toHaveProperty("fail"); + if (migratedState.fail) { + // TypeScript guard for type safety + expect(migratedState.fail).toHaveProperty("showLockModal"); + // Validate showLockModal + expect(migratedState.fail.showLockModal).toBe(false); + } + }); + + it("should add showLockModal=true to fail object if fail exists and remainingAttempts <= 3", () => { + // Setup state with undefined fail + const prevStateThatShouldHaveShowLockSetToTrue: MockState = { + ...previousState, + fail: { + nextLegalAttempt: lastAttempt, + remainingAttempts: 3, + timespanBetweenAttempts: 30 + } + }; + + // Execute the migration + const migratedState = identificationStateMigration["0"]( + prevStateThatShouldHaveShowLockSetToTrue + ) as MockState; + + // Assertions + expect(migratedState).toHaveProperty("fail"); + if (migratedState.fail) { + // TypeScript guard for type safety + expect(migratedState.fail).toHaveProperty("showLockModal"); + // Validate showLockModal value + expect(migratedState.fail.showLockModal).toBe(true); + } + }); + + it("should handle cases where fail is undefined or null", () => { + // Setup state with undefined fail + const prevStateWithUndefinedFail: MockState = { + ...previousState, + fail: undefined + }; + + // Execute migration + const migratedStateWithUndefinedFail = identificationStateMigration["0"]( + prevStateWithUndefinedFail + ) as MockState; + + // Assertions for undefined fail + expect(migratedStateWithUndefinedFail.fail).toBeUndefined(); + }); +}); diff --git a/ts/store/reducers/__tests__/profile.test.ts b/ts/store/reducers/__tests__/profile.test.ts index e8fc9ef279d..2c26f7336df 100644 --- a/ts/store/reducers/__tests__/profile.test.ts +++ b/ts/store/reducers/__tests__/profile.test.ts @@ -4,11 +4,13 @@ import mockedProfile from "../../../__mocks__/initializedProfile"; import { hasProfileEmail, isProfileEmailValidated, + isProfileEmailValidatedSelector, isProfileFirstOnBoarding, profileEmailSelector, ProfileState } from "../profile"; import { ServicesPreferencesModeEnum } from "../../../../definitions/backend/ServicesPreferencesMode"; +import { GlobalState } from "../types"; describe("email profile selector", () => { const potProfile: ProfileState = pot.some(mockedProfile); @@ -67,3 +69,222 @@ describe("email profile selector", () => { expect(isProfileFirstOnBoarding(potProfile.value)).toStrictEqual(false); }); }); + +describe("isProfileEmailValidatedSelector", () => { + it("should return false for pot.none profile", () => { + const state = { + profile: pot.none + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.noneLoading profile", () => { + const state = { + profile: pot.noneLoading + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.noneUpdating profile", () => { + const state = { + profile: pot.noneUpdating({ + email: "namesurname@domain.com", + is_email_validated: true + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.noneError profile", () => { + const state = { + profile: pot.noneError(new Error()) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.some profile with undefined email", () => { + const state = { + profile: pot.some({ + is_email_validated: true + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.some profile with defined email but undefined is_email_validated", () => { + const state = { + profile: pot.some({ + email: "namesurname@domain.com" + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.some profile with defined email but false is_email_validated", () => { + const state = { + profile: pot.some({ + email: "namesurname@domain.com", + is_email_validated: false + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return true for pot.some profile with defined email and validated email", () => { + const state = { + profile: pot.some({ + email: "namesurname@domain.com", + is_email_validated: true + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(true); + }); + it("should return false for pot.someLoading profile with undefined email", () => { + const state = { + profile: pot.someLoading({ + is_email_validated: true + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someLoading profile with defined email but undefined is_email_validated", () => { + const state = { + profile: pot.someLoading({ + email: "namesurname@domain.com" + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someLoading profile with defined email but false is_email_validated", () => { + const state = { + profile: pot.someLoading({ + email: "namesurname@domain.com", + is_email_validated: false + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return true for pot.someLoading profile with defined email and validated email", () => { + const state = { + profile: pot.someLoading({ + email: "namesurname@domain.com", + is_email_validated: true + }) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(true); + }); + it("should return false for pot.someUpdating profile with undefined email", () => { + const state = { + profile: pot.someUpdating( + { + is_email_validated: true + }, + { + email: "namesurname@domain.com", + is_email_validated: true + } + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someUpdating profile with defined email but undefined is_email_validated", () => { + const state = { + profile: pot.someUpdating( + { + email: "namesurname@domain.com" + }, + { + email: "namesurname@domain.com", + is_email_validated: true + } + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someUpdating profile with defined email but false is_email_validated", () => { + const state = { + profile: pot.someUpdating( + { + email: "namesurname@domain.com", + is_email_validated: false + }, + { + email: "namesurname@domain.com", + is_email_validated: true + } + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return true for pot.someUpdating profile with defined email and validated email", () => { + const state = { + profile: pot.someUpdating( + { + email: "namesurname@domain.com", + is_email_validated: true + }, + {} + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(true); + }); + it("should return false for pot.someError profile with undefined email", () => { + const state = { + profile: pot.someError( + { + is_email_validated: true + }, + new Error() + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someError profile with defined email but undefined is_email_validated", () => { + const state = { + profile: pot.someError( + { + email: "namesurname@domain.com" + }, + new Error() + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return false for pot.someError profile with defined email but false is_email_validated", () => { + const state = { + profile: pot.someError( + { + email: "namesurname@domain.com", + is_email_validated: false + }, + new Error() + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(false); + }); + it("should return true for pot.someError profile with defined email and validated email", () => { + const state = { + profile: pot.someError( + { + email: "namesurname@domain.com", + is_email_validated: true + }, + new Error() + ) + } as GlobalState; + const isProfileEmailValidated = isProfileEmailValidatedSelector(state); + expect(isProfileEmailValidated).toBe(true); + }); +}); diff --git a/ts/store/reducers/assistanceTools.ts b/ts/store/reducers/assistanceTools.ts index ec01f4826c8..f58e131b0b0 100644 --- a/ts/store/reducers/assistanceTools.ts +++ b/ts/store/reducers/assistanceTools.ts @@ -1,5 +1,4 @@ import { combineReducers } from "redux"; -import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; import { Action } from "../actions/types"; import zendeskReducer, { @@ -10,7 +9,6 @@ import { canShowHelp } from "../../utils/supportAssistance"; import { assistanceToolConfigSelector } from "./backendStatus"; -import { isProfileEmailValidatedSelector, profileSelector } from "./profile"; export type AssistanceToolsState = { zendesk: ZendeskState; @@ -21,17 +19,12 @@ const assistanceToolsReducer = combineReducers({ }); // This selector contains the logic to show or not the help button: -// if remote FF is zendesk + ff local + the profile is not potSome or the email is validated +// if remote FF is zendesk + ff local export const canShowHelpSelector = createSelector( assistanceToolConfigSelector, - profileSelector, - isProfileEmailValidatedSelector, - (assistanceToolConfig, profile, isProfileEmailValidated): boolean => { + (assistanceToolConfig): boolean => { const remoteTool = assistanceToolRemoteConfig(assistanceToolConfig); - return canShowHelp( - remoteTool, - !pot.isSome(profile) || isProfileEmailValidated - ); + return canShowHelp(remoteTool); } ); diff --git a/ts/store/reducers/authentication.ts b/ts/store/reducers/authentication.ts index fe30867947b..bb21d7be7e5 100644 --- a/ts/store/reducers/authentication.ts +++ b/ts/store/reducers/authentication.ts @@ -210,6 +210,9 @@ export const tokenFromNameSelector = ( ) ); +export const loggedInIdpSelector = (state: GlobalState) => + isLoggedIn(state.authentication) ? state.authentication.idp : undefined; + export const isLoggedInWithTestIdpSelector = (state: GlobalState) => isLoggedIn(state.authentication) && state.authentication.idp.isTestIdp; diff --git a/ts/store/reducers/emailValidation.ts b/ts/store/reducers/emailValidation.ts index 24f667b11a3..97f000d92b1 100644 --- a/ts/store/reducers/emailValidation.ts +++ b/ts/store/reducers/emailValidation.ts @@ -9,6 +9,8 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; import { acknowledgeOnEmailValidation, + emailValidationPollingStart, + emailValidationPollingStop, setEmailCheckAtStartupFailure, startEmailValidation } from "../actions/profile"; @@ -19,16 +21,17 @@ export type EmailValidationState = { sendEmailValidationRequest: pot.Pot; acknowledgeOnEmailValidated: O.Option; emailCheckAtStartupFailed: O.Option; + isEmailValidationPollingRunning: boolean; }; const INITIAL_STATE: EmailValidationState = { sendEmailValidationRequest: pot.none, acknowledgeOnEmailValidated: O.none, - emailCheckAtStartupFailed: O.none + emailCheckAtStartupFailed: O.none, + isEmailValidationPollingRunning: false }; // Selector - // return the pot of email validation export const emailValidationSelector = ( state: GlobalState @@ -55,6 +58,10 @@ const reducer = ( return { ...state, acknowledgeOnEmailValidated: action.payload }; case getType(setEmailCheckAtStartupFailure): return { ...state, emailCheckAtStartupFailed: action.payload }; + case getType(emailValidationPollingStart): + return { ...state, isEmailValidationPollingRunning: true }; + case getType(emailValidationPollingStop): + return { ...state, isEmailValidationPollingRunning: false }; default: return state; } diff --git a/ts/store/reducers/entities/calendarEvents/calendarEventsByMessageId.ts b/ts/store/reducers/entities/calendarEvents/calendarEventsByMessageId.ts index 42848084e98..d9d9850e910 100644 --- a/ts/store/reducers/entities/calendarEvents/calendarEventsByMessageId.ts +++ b/ts/store/reducers/entities/calendarEvents/calendarEventsByMessageId.ts @@ -47,8 +47,9 @@ const reducer = ( }; // Selectors -export const calendarEventByMessageIdSelector = - (messageId: string) => (state: GlobalState) => - state.entities.calendarEvents.byMessageId[messageId]; +export const calendarEventByMessageIdSelector = ( + state: GlobalState, + messageId: string +) => state.entities.calendarEvents.byMessageId[messageId]; export default reducer; diff --git a/ts/store/reducers/entities/services/__tests__/index.test.ts b/ts/store/reducers/entities/services/__tests__/index.test.ts index 79eb76c59cb..bcbb3cd5a06 100644 --- a/ts/store/reducers/entities/services/__tests__/index.test.ts +++ b/ts/store/reducers/entities/services/__tests__/index.test.ts @@ -24,7 +24,7 @@ import { ServiceName } from "../../../../../../definitions/backend/ServiceName"; import { ServiceTuple } from "../../../../../../definitions/backend/ServiceTuple"; import { UserMetadataState } from "../../../userMetadata"; import { OrganizationsState } from "../../organizations"; -import { ServicesByIdState } from "../servicesById"; +import { ServicesByIdState } from "../../../../../features/services/store/reducers/servicesById"; import { VisibleServicesState } from "../visibleServices"; import { ServiceScopeEnum } from "../../../../../../definitions/backend/ServiceScope"; import { StandardServiceCategoryEnum } from "../../../../../../definitions/backend/StandardServiceCategory"; diff --git a/ts/store/reducers/entities/services/__tests__/servicePreference.test.ts b/ts/store/reducers/entities/services/__tests__/servicePreference.test.ts deleted file mode 100644 index 9631a34bd56..00000000000 --- a/ts/store/reducers/entities/services/__tests__/servicePreference.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import servicePreferenceReducer, { - ServicePreferenceState -} from "../servicePreference"; -import { - loadServicePreference, - upsertServicePreference -} from "../../../../actions/services/servicePreference"; -import { - ServicePreference, - ServicePreferenceResponse, - WithServiceID -} from "../../../../../types/services/ServicePreferenceResponse"; -import { - getNetworkError, - getNetworkErrorMessage, - NetworkError -} from "../../../../../utils/errors"; -import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; - -const initialState: ServicePreferenceState = pot.none; - -describe("servicePreferenceReducer", () => { - it("should handle the load request of servicePreference", () => { - const action = loadServicePreference.request("s1" as ServiceId); - - const updatedState = servicePreferenceReducer(initialState, action); - - expect(updatedState).toMatchObject(pot.noneLoading); - }); - - it("should handle the success load of servicePreference", () => { - const servicePreferenceResponse: ServicePreferenceResponse = { - id: "s1" as ServiceId, - kind: "success", - value: { - inbox: true, - push: true, - email: false, - can_access_message_read_status: false, - settings_version: 0 - } - }; - - const action = loadServicePreference.success(servicePreferenceResponse); - - const updatedState = servicePreferenceReducer(initialState, action); - - expect(updatedState).toMatchObject(pot.some(servicePreferenceResponse)); - }); - - it("should handle the updating request and success case of servicePreference", () => { - const state: ServicePreferenceState = pot.some({ - id: "s1" as ServiceId, - kind: "success", - value: { - inbox: true, - push: true, - email: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - - const updatingResponse: WithServiceID = { - id: "s1" as ServiceId, - inbox: true, - push: true, - email: true, - can_access_message_read_status: true, - settings_version: 0 - }; - - const requestUpsert = upsertServicePreference.request(updatingResponse); - - const updatingState = servicePreferenceReducer(state, requestUpsert); - - expect(updatingState).toMatchObject( - pot.someUpdating(state.value, { - id: "s1" as ServiceId, - kind: "success", - value: { - inbox: true, - push: true, - email: true, - can_access_message_read_status: true, - settings_version: 0 - } - }) - ); - - const successUpsert = upsertServicePreference.success({ - id: "s1" as ServiceId, - kind: "success", - value: { - inbox: true, - push: true, - email: true, - can_access_message_read_status: true, - settings_version: 0 - } - }); - - const updatedState = servicePreferenceReducer(state, successUpsert); - - expect(updatedState).toMatchObject( - pot.some({ - id: "s1" as ServiceId, - kind: "success", - value: { - inbox: true, - push: true, - email: true, - can_access_message_read_status: true, - settings_version: 0 - } - }) - ); - }); -}); - -it("should handle the error load of servicePreference", () => { - const servicePreferenceError: WithServiceID = { - id: "s1" as ServiceId, - ...getNetworkError(new Error("GenericError")) - }; - - const action = loadServicePreference.failure(servicePreferenceError); - - const updatedState = servicePreferenceReducer(initialState, action); - - expect(updatedState).toMatchObject( - pot.noneError(new Error(getNetworkErrorMessage(servicePreferenceError))) - ); -}); diff --git a/ts/store/reducers/entities/services/__tests__/servicesById.test.ts b/ts/store/reducers/entities/services/__tests__/servicesById.test.ts deleted file mode 100644 index 0b916e8cbd8..00000000000 --- a/ts/store/reducers/entities/services/__tests__/servicesById.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Tuple2 } from "@pagopa/ts-commons/lib/tuples"; - -import { removeServiceTuples } from "../../../../actions/services"; -import servicesByIdReducer from "../servicesById"; - -const initialState = { - s1: {}, - s2: {}, - s3: {}, - s4: {}, - s5: {} -}; - -describe("servicesById", () => { - it("should handle removeServiceTuples correctly", () => { - const action = removeServiceTuples([ - Tuple2("s2", "a"), - Tuple2("s3", "b"), - // Not existing serviceId - Tuple2("s6", "b") - ]); - - const expectedState = { - s1: {}, - s4: {}, - s5: {} - }; - - const obtainedState = servicesByIdReducer(initialState as any, action); - - expect(obtainedState).toMatchObject(expectedState); - }); -}); diff --git a/ts/store/reducers/entities/services/index.ts b/ts/store/reducers/entities/services/index.ts index 716e672ba0e..aa8f6b630e1 100644 --- a/ts/store/reducers/entities/services/index.ts +++ b/ts/store/reducers/entities/services/index.ts @@ -1,12 +1,20 @@ /** * Services reducer */ -import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; import { combineReducers } from "redux"; import { createSelector } from "reselect"; -import { pipe } from "fp-ts/lib/function"; import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; +import { ServiceScopeEnum } from "../../../../../definitions/backend/ServiceScope"; +import servicePreferenceReducer, { + ServicePreferenceState +} from "../../../../features/services/store/reducers/servicePreference"; +import servicesByIdReducer, { + servicesByIdSelector, + ServicesByIdState +} from "../../../../features/services/store/reducers/servicesById"; import { isDefined } from "../../../../utils/guards"; import { isVisibleService } from "../../../../utils/services"; import { Action } from "../../../actions/types"; @@ -16,7 +24,6 @@ import { organizationNamesByFiscalCodeSelector, OrganizationNamesByFiscalCodeState } from "../organizations/organizationsByFiscalCodeReducer"; -import { ServiceScopeEnum } from "../../../../../definitions/backend/ServiceScope"; import { firstLoadingReducer, FirstLoadingState, @@ -26,10 +33,6 @@ import readServicesByIdReducer, { readServicesByIdSelector, ReadStateByServicesId } from "./readStateByServiceId"; -import servicesByIdReducer, { - servicesByIdSelector, - ServicesByIdState -} from "./servicesById"; import { serviceIdsByOrganizationFiscalCodeReducer, ServiceIdsByOrganizationFiscalCodeState @@ -39,9 +42,6 @@ import { visibleServicesSelector, VisibleServicesState } from "./visibleServices"; -import servicePreferenceReducer, { - ServicePreferenceState -} from "./servicePreference"; export type ServicesState = Readonly<{ // Section to hold the preference for services diff --git a/ts/store/reducers/entities/services/servicePreference.ts b/ts/store/reducers/entities/services/servicePreference.ts deleted file mode 100644 index 520d441be2d..00000000000 --- a/ts/store/reducers/entities/services/servicePreference.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { getType } from "typesafe-actions"; -import { Action } from "../../../actions/types"; -import { - loadServicePreference, - upsertServicePreference -} from "../../../actions/services/servicePreference"; -import { ServicePreferenceResponse } from "../../../../types/services/ServicePreferenceResponse"; -import { getNetworkErrorMessage } from "../../../../utils/errors"; -import { GlobalState } from "../../types"; - -export type ServicePreferenceState = pot.Pot; - -// Reducer to handle specific service contact preferences (inbox, push, emails) -const servicePreferenceReducer = ( - state: ServicePreferenceState = pot.none, - action: Action -): ServicePreferenceState => { - switch (action.type) { - case getType(loadServicePreference.request): - return pot.toLoading(state); - case getType(upsertServicePreference.request): - const { id, ...payload } = action.payload; - - return pot.toUpdating(state, { - id, - kind: "success", - value: payload - }); - case getType(loadServicePreference.success): - case getType(upsertServicePreference.success): - return pot.some(action.payload); - case getType(loadServicePreference.failure): - case getType(upsertServicePreference.failure): - return pot.toError( - state, - new Error(getNetworkErrorMessage(action.payload)) - ); - } - return state; -}; - -export default servicePreferenceReducer; - -// Selectors -export const servicePreferenceSelector = ( - state: GlobalState -): ServicePreferenceState => state.entities.services.servicePreference; diff --git a/ts/store/reducers/identification.ts b/ts/store/reducers/identification.ts index 9ee81dd984c..73e623b7cc7 100644 --- a/ts/store/reducers/identification.ts +++ b/ts/store/reducers/identification.ts @@ -7,6 +7,7 @@ import { PinString } from "../../types/PinString"; import { identificationCancel, identificationFailure, + identificationHideLockModal, identificationReset, identificationStart, identificationSuccess @@ -66,6 +67,7 @@ export type IdentificationFailData = { remainingAttempts: number; nextLegalAttempt: Date; timespanBetweenAttempts: number; + showLockModal?: boolean; }; export type IdentificationState = { @@ -84,6 +86,20 @@ export const INITIAL_STATE: IdentificationState = { fail: undefined }; +export const fillShowLockModal = (actualErrorData: IdentificationFailData) => { + // showLockModal is true if the time gap between now and the next legal attempt + // is less than the timespanBetweenAttempts and the remaining attempts are less than 3 + const timeGap = + new Date().getTime() - actualErrorData.nextLegalAttempt.getTime(); + const showLockModal = + actualErrorData.remainingAttempts <= 3 && + timeGap < actualErrorData.timespanBetweenAttempts * 1000; + return { + ...actualErrorData, + showLockModal + }; +}; + const nextErrorData = ( errorData: IdentificationFailData ): IdentificationFailData => { @@ -100,7 +116,8 @@ const nextErrorData = ( return { nextLegalAttempt: new Date(Date.now() + newTimespan * 1000), remainingAttempts: nextRemainingAttempts, - timespanBetweenAttempts: newTimespan + timespanBetweenAttempts: newTimespan, + showLockModal: nextRemainingAttempts <= 3 }; }; @@ -136,6 +153,18 @@ const reducer = ( case getType(identificationReset): return INITIAL_STATE; + case getType(identificationHideLockModal): + const failData = state.fail + ? { + ...state.fail, + showLockModal: false + } + : undefined; + return { + ...state, + fail: failData + }; + case getType(identificationFailure): const newErrorData = pipe( state.fail, @@ -144,7 +173,8 @@ const reducer = ( () => ({ nextLegalAttempt: new Date(), remainingAttempts: maxAttempts - 1, - timespanBetweenAttempts: 0 + timespanBetweenAttempts: 0, + showLockModal: false }), errorData => nextErrorData(errorData) ) diff --git a/ts/store/reducers/index.ts b/ts/store/reducers/index.ts index 11e8a2ac658..50b1da8ee2e 100644 --- a/ts/store/reducers/index.ts +++ b/ts/store/reducers/index.ts @@ -3,7 +3,15 @@ */ import AsyncStorage from "@react-native-async-storage/async-storage"; import { combineReducers, Reducer } from "redux"; -import { PersistConfig, persistReducer, purgeStoredState } from "redux-persist"; +import { + createMigrate, + MigrationManifest, + PersistConfig, + PersistedState, + PersistPartial, + persistReducer, + purgeStoredState +} from "redux-persist"; import { isActionOf } from "typesafe-actions"; import { versionInfoReducer } from "../../common/versionInfo/store/reducers/versionInfo"; import bonusReducer from "../../features/bonus/common/store/reducers"; @@ -20,6 +28,7 @@ import createSecureStorage from "../storages/keychain"; import { DateISO8601Transform } from "../transforms/dateISO8601Tranform"; import { whatsNewInitialState } from "../../features/whatsnew/store/reducers"; import { fastLoginOptInInitialState } from "../../features/fastLogin/store/reducers/optInReducer"; +import { isDevEnv } from "../../utils/environment"; import appStateReducer from "./appState"; import assistanceToolsReducer from "./assistanceTools"; import authenticationReducer, { @@ -41,6 +50,7 @@ import entitiesReducer, { } from "./entities"; import identificationReducer, { IdentificationState, + fillShowLockModal, INITIAL_STATE as identificationInitialState } from "./identification"; import installationReducer from "./installation"; @@ -68,12 +78,29 @@ export const authenticationPersistConfig: PersistConfig = { blacklist: ["deepLink"] }; +export const IDENTIFICATION_STATE_MIGRATION_VERSION = 0; +export const identificationStateMigration: MigrationManifest = { + // version 0 + // we added showLockModal + "0": (state: PersistedState) => { + const previousState = state as IdentificationState & PersistPartial; + const failData = previousState.fail + ? fillShowLockModal(previousState.fail) + : undefined; + return { + ...previousState, + fail: failData + } as PersistedState; + } +}; // A custom configuration to store the fail information of the identification section export const identificationPersistConfig: PersistConfig = { key: "identification", storage: AsyncStorage, blacklist: ["progress"], - transforms: [DateISO8601Transform] + transforms: [DateISO8601Transform], + version: IDENTIFICATION_STATE_MIGRATION_VERSION, + migrate: createMigrate(identificationStateMigration, { debug: isDevEnv }) }; /** diff --git a/ts/store/reducers/persistedPreferences.ts b/ts/store/reducers/persistedPreferences.ts index 106df12a97b..ea9d6bb7f6f 100644 --- a/ts/store/reducers/persistedPreferences.ts +++ b/ts/store/reducers/persistedPreferences.ts @@ -19,7 +19,9 @@ import { serviceAlertDisplayedOnceSuccess, preferencesPnTestEnvironmentSetEnabled, preferencesIdPayTestSetEnabled, - preferencesDesignSystemSetEnabled + preferencesDesignSystemSetEnabled, + preferencesNewWalletSectionSetEnabled, + preferencesItWalletTestSetEnabled } from "../actions/persistedPreferences"; import { Action } from "../actions/types"; import { differentProfileLoggedIn } from "../actions/crossSessions"; @@ -38,7 +40,14 @@ export type PersistedPreferencesState = Readonly<{ isMixpanelEnabled: boolean | null; isPnTestEnabled: boolean; isIdPayTestEnabled?: boolean; + // 'isDesignSystemEnabled' has been introduced without a migration + // (PR https://github.com/pagopa/io-app/pull/4427) so there are cases + // where its value is `undefined` (when the user updates the app without + // changing the variable value later). Typescript cannot detect this so + // be sure to handle such case when reading and using this value isDesignSystemEnabled: boolean; + isNewWalletSectionEnabled: boolean; + isItWalletTestEnabled?: boolean; }>; export const initialPreferencesState: PersistedPreferencesState = { @@ -52,7 +61,9 @@ export const initialPreferencesState: PersistedPreferencesState = { isMixpanelEnabled: null, isPnTestEnabled: false, isIdPayTestEnabled: false, - isDesignSystemEnabled: false + isDesignSystemEnabled: false, + isNewWalletSectionEnabled: false, + isItWalletTestEnabled: false }; export default function preferencesReducer( @@ -148,6 +159,20 @@ export default function preferencesReducer( }; } + if (isActionOf(preferencesNewWalletSectionSetEnabled, action)) { + return { + ...state, + isNewWalletSectionEnabled: action.payload.isNewWalletSectionEnabled + }; + } + + if (isActionOf(preferencesItWalletTestSetEnabled, action)) { + return { + ...state, + isItWalletTestEnabled: action.payload.isItWalletTestEnabled + }; + } + return state; } @@ -182,8 +207,19 @@ export const isPnTestEnabledSelector = (state: GlobalState) => export const isIdPayTestEnabledSelector = (state: GlobalState) => !!state.persistedPreferences?.isIdPayTestEnabled; +// 'isDesignSystemEnabled' has been introduced without a migration +// (PR https://github.com/pagopa/io-app/pull/4427) so there are cases +// where its value is `undefined` (when the user updates the app without +// changing the variable value later). Typescript cannot detect this so +// we must make sure that the signature's return type is respected export const isDesignSystemEnabledSelector = (state: GlobalState) => - state.persistedPreferences.isDesignSystemEnabled; + state.persistedPreferences.isDesignSystemEnabled ?? false; + +export const isNewWalletSectionEnabledSelector = (state: GlobalState) => + state.persistedPreferences?.isNewWalletSectionEnabled ?? false; + +export const isItWalletTestEnabledSelector = (state: GlobalState) => + !!state.persistedPreferences?.isItWalletTestEnabled; // returns the preferred language as an Option from the persisted store export const preferredLanguageSelector = createSelector< diff --git a/ts/store/reducers/profile.ts b/ts/store/reducers/profile.ts index 715fc9bb1ad..c113f7c627d 100644 --- a/ts/store/reducers/profile.ts +++ b/ts/store/reducers/profile.ts @@ -3,6 +3,7 @@ * It only manages SUCCESS actions because all UI state properties (like loading/error) * are managed by different global reducers. */ +import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; @@ -165,13 +166,23 @@ export const isProfileFirstOnBoardingSelector = createSelector( ); // return true if the profile pot is some and its field is_email_validated exists and it's true -export const isProfileEmailValidatedSelector = createSelector( - profileSelector, - (profile: ProfileState): boolean => - pot.getOrElse( - pot.map(profile, p => hasProfileEmail(p) && isProfileEmailValidated(p)), - false - ) +export const isProfileEmailValidatedSelector = (state: GlobalState) => + pipe( + state, + profileSelector, + profileStatusPot => + pot.map( + profileStatusPot, + profileStatus => + hasProfileEmail(profileStatus) && + isProfileEmailValidated(profileStatus) + ), + profileEmailValidatedPot => pot.getOrElse(profileEmailValidatedPot, false) + ); + +export const isEmailValidatedSelector = createSelector( + isProfileEmailValidatedSelector, + isEmailValidated => isEmailValidated ); // return preferences @@ -288,6 +299,7 @@ const reducer = ( return pot.some({ ...currentProfile, email: newProfile.email, + last_app_version: newProfile.last_app_version, is_inbox_enabled: newProfile.is_inbox_enabled === true, is_email_enabled: newProfile.is_email_enabled === true, is_email_validated: newProfile.is_email_validated === true, diff --git a/ts/store/sagas/emailValidationPollingSaga.ts b/ts/store/sagas/emailValidationPollingSaga.ts new file mode 100644 index 00000000000..fba76e2ed6c --- /dev/null +++ b/ts/store/sagas/emailValidationPollingSaga.ts @@ -0,0 +1,31 @@ +// watch for all actions regarding Zendesk +import { takeLatest, call, put, take, select } from "typed-redux-saga/macro"; +import { Millisecond } from "@pagopa/ts-commons/lib/units"; +import { startTimer } from "../../utils/timer"; +import { + emailValidationPollingStart, + profileLoadRequest, + profileLoadSuccess +} from "../actions/profile"; +import { emailValidationSelector } from "../reducers/emailValidation"; + +const GET_PROFILE_POLLING_INTERVAL = 5000 as Millisecond; + +function* emailValidationPollingLoop() { + // eslint-disable-next-line functional/no-let + let profilePollingIsRunning = true; + + while (profilePollingIsRunning) { + yield* call(startTimer, GET_PROFILE_POLLING_INTERVAL); + + yield* put(profileLoadRequest()); + yield* take(profileLoadSuccess); + const isEmailValidationSelector = yield* select(emailValidationSelector); + profilePollingIsRunning = + isEmailValidationSelector.isEmailValidationPollingRunning; + } +} + +export function* watchEmailValidationSaga() { + yield* takeLatest(emailValidationPollingStart, emailValidationPollingLoop); +} diff --git a/ts/utils/__tests__/supportAssistance.test.ts b/ts/utils/__tests__/supportAssistance.test.ts index ed6499c7d13..f26b926ada1 100644 --- a/ts/utils/__tests__/supportAssistance.test.ts +++ b/ts/utils/__tests__/supportAssistance.test.ts @@ -29,16 +29,12 @@ describe("anonymousAssistanceAddress", () => { describe("canShowHelp", () => { it("if assistanceTool is Zendesk, should return true if the email is validated", () => { - expect(canShowHelp(ToolEnum.zendesk, true)).toBeTruthy(); - expect(canShowHelp(ToolEnum.zendesk, false)).toBeFalsy(); + expect(canShowHelp(ToolEnum.zendesk)).toBeTruthy(); }); it("if assistanceTool is instabug, web or none, should return false", () => { - expect(canShowHelp(ToolEnum.instabug, true)).toBeFalsy(); - expect(canShowHelp(ToolEnum.instabug, false)).toBeFalsy(); - expect(canShowHelp(ToolEnum.web, true)).toBeFalsy(); - expect(canShowHelp(ToolEnum.none, true)).toBeFalsy(); - expect(canShowHelp(ToolEnum.web, false)).toBeFalsy(); - expect(canShowHelp(ToolEnum.none, false)).toBeFalsy(); + expect(canShowHelp(ToolEnum.instabug)).toBeFalsy(); + expect(canShowHelp(ToolEnum.web)).toBeFalsy(); + expect(canShowHelp(ToolEnum.none)).toBeFalsy(); }); }); diff --git a/ts/utils/analytics.ts b/ts/utils/analytics.ts index 5b55f71390f..a33877960dc 100644 --- a/ts/utils/analytics.ts +++ b/ts/utils/analytics.ts @@ -179,6 +179,26 @@ export function trackLollipopIdpLoginFailure(reason: string) { }); } +export function trackLollipopIsKeyStrongboxBackedSuccess( + isStrongboxBacked: boolean +) { + void mixpanelTrack( + "LOLLIPOP_IS_KEY_STRONGBOX_BACKED_SUCCESS", + buildEventProperties("TECH", undefined, { + isStrongboxBacked + }) + ); +} + +export function trackLollipopIsKeyStrongboxBackedFailure(reason: string) { + void mixpanelTrack( + "LOLLIPOP_IS_KEY_STRONGBOX_BACKED_FAILURE", + buildEventProperties("KO", undefined, { + reason + }) + ); +} + // End of lollipop events // SPID Login @@ -190,9 +210,9 @@ export function trackSpidLoginError( if (isLoginUtilsError(e)) { void mixpanelTrack(eventName, { idp: idpName, - code: e.userInfo.StatusCode, - description: e.userInfo.Error, - domain: e.userInfo.URL + code: e.userInfo?.statusCode, + description: e.userInfo?.error, + domain: e.userInfo?.url }); } else { const error = e as Error; diff --git a/ts/utils/calendar.ts b/ts/utils/calendar.ts index 8f1cab98ccb..748fdbfb2fb 100644 --- a/ts/utils/calendar.ts +++ b/ts/utils/calendar.ts @@ -3,12 +3,12 @@ import * as TE from "fp-ts/lib/TaskEither"; import * as E from "fp-ts/lib/Either"; import RNCalendarEvents, { Calendar } from "react-native-calendar-events"; import { Platform } from "react-native"; -import { pipe } from "fp-ts/lib/function"; +import { identity, pipe } from "fp-ts/lib/function"; +import { CreatedMessageWithContentAndAttachments } from "../../definitions/backend/CreatedMessageWithContentAndAttachments"; import { TranslationKeys } from "../../locales/locales"; import I18n from "../i18n"; import { AddCalendarEventPayload } from "../store/actions/calendarEvents"; import { CalendarEvent } from "../store/reducers/entities/calendarEvents/calendarEventsByMessageId"; -import { CreatedMessageWithContentAndAttachments } from "../../definitions/backend/CreatedMessageWithContentAndAttachments"; import { formatDateAsReminder } from "./dates"; import { showToast } from "./showToast"; @@ -97,7 +97,7 @@ export function convertLocalCalendarName(calendarTitle: string) { * and right is a boolean -> true === the is in calendar * @param eventId */ -export const isEventInCalendar = ( +export const legacyIsEventInCalendar = ( eventId: string ): TE.TaskEither => { const authTask = TE.tryCatch( @@ -208,3 +208,67 @@ export const removeCalendarEventFromDeviceCalendar = ( showToast(I18n.t("messages.cta.reminderRemoveFailure"), "danger"); } }; + +/** + * Check and request the permission to access the device calendar + * @returns a boolean that is true if the permission is granted + */ +export const requestCalendarPermission = async (): Promise => { + const checkResult = await RNCalendarEvents.checkPermissions(); + if (checkResult === "authorized") { + return true; + } + + const requestStatus = await RNCalendarEvents.requestPermissions(); + return requestStatus === "authorized"; +}; + +/** + * Check if the event is in the device calendar + */ +export const isEventInCalendar = (eventId: string) => + pipe( + TE.tryCatch(() => requestCalendarPermission(), E.toError), + TE.chain(TE.fromPredicate(identity, () => Error("Permission not granted"))), + TE.chain(() => + TE.tryCatch(() => RNCalendarEvents.findEventById(eventId), E.toError) + ), + TE.map(ev => ev !== null) + ); + +/** + * Add an event to the device calendar + */ +export const saveEventToDeviceCalendarTask = ( + calendarId: string, + dueDate: Date, + title: string +) => + TE.tryCatch( + () => + RNCalendarEvents.saveEvent(title, { + calendarId, + startDate: dueDate.toISOString(), + endDate: dueDate.toISOString(), + allDay: true, + alarms: [] + }), + E.toError + ); + +/** + * Remove an event from the device calendar + */ +export const removeEventFromDeviceCalendarTask = (eventId: string) => + pipe( + TE.tryCatch(() => RNCalendarEvents.removeEvent(eventId), E.toError), + TE.map(_ => eventId) + ); + +/** + * Find the device calendars + */ +export const findDeviceCalendarsTask = TE.tryCatch( + () => RNCalendarEvents.findCalendars(), + E.toError +); diff --git a/ts/utils/clipboard.ts b/ts/utils/clipboard.ts index c27b9f792a4..ee9e5ef120d 100644 --- a/ts/utils/clipboard.ts +++ b/ts/utils/clipboard.ts @@ -1,7 +1,7 @@ import Clipboard from "@react-native-clipboard/clipboard"; +import { IOToast } from "@pagopa/io-app-design-system"; import I18n from "../i18n"; -import { IOToast } from "../components/Toast"; /** * Copy a text to the device clipboard and give a feedback. diff --git a/ts/utils/dates.ts b/ts/utils/dates.ts index 41b5b99c83a..faba844cd94 100644 --- a/ts/utils/dates.ts +++ b/ts/utils/dates.ts @@ -325,8 +325,12 @@ export const toAndroidCacheTimestamp = () => * This function returns a Date object from a string in format "YYYYMM" * @param expiryDate */ -export const getDateFromExpiryDate = (expiryDate: string): Date => { - const year = +expiryDate.slice(0, 4); - const month = +expiryDate.slice(4, 6); - return new Date(year, month - 1); +export const getDateFromExpiryDate = (expiryDate: string): Date | undefined => { + try { + const year = +expiryDate.slice(0, 4); + const month = +expiryDate.slice(4, 6); + return new Date(year, month - 1); + } catch { + return undefined; + } }; diff --git a/ts/utils/hooks/bottomSheet.tsx b/ts/utils/hooks/bottomSheet.tsx index 4101034eb6d..c9dfded9160 100644 --- a/ts/utils/hooks/bottomSheet.tsx +++ b/ts/utils/hooks/bottomSheet.tsx @@ -6,6 +6,11 @@ import { useBottomSheetModal } from "@gorhom/bottom-sheet"; import { BottomSheetFooterProps } from "@gorhom/bottom-sheet/lib/typescript/components/bottomSheetFooter"; +import { + IOBottomSheetHeaderRadius, + IOSpacingScale, + IOVisualCostants +} from "@pagopa/io-app-design-system"; import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray"; import * as React from "react"; import { useCallback, useEffect, useState } from "react"; @@ -18,11 +23,6 @@ import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { - IOBottomSheetHeaderRadius, - IOSpacingScale, - IOVisualCostants -} from "@pagopa/io-app-design-system"; import { BottomSheetHeader } from "../../components/bottomSheet/BottomSheetHeader"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { useHardwareBackButtonToDismiss } from "../../hooks/useHardwareBackButton"; @@ -40,6 +40,7 @@ const styles = StyleSheet.create({ bottomSheet: { borderTopRightRadius: IOBottomSheetHeaderRadius, borderTopLeftRadius: IOBottomSheetHeaderRadius, + borderCurve: "continuous", // Don't delete the overflow property // oterwise the above borderRadius won't work overflow: "hidden" diff --git a/ts/utils/hooks/theme/index.ts b/ts/utils/hooks/theme/index.ts new file mode 100644 index 00000000000..d3a2d00aa80 --- /dev/null +++ b/ts/utils/hooks/theme/index.ts @@ -0,0 +1,21 @@ +import { IOColors, useIOTheme } from "@pagopa/io-app-design-system"; + +export const useAppBackgroundAccentColor = (): IOColors => { + const theme = useIOTheme(); + return theme["appBackground-accent"]; +}; + +export const useAppBackgroundAccentColorName = (): string => { + const color = useAppBackgroundAccentColor(); + return IOColors[color]; +}; + +export const useInteractiveElementDefaultColor = (): IOColors => { + const theme = useIOTheme(); + return theme["interactiveElem-default"]; +}; + +export const useInteractiveElementDefaultColorName = (): string => { + const color = useInteractiveElementDefaultColor(); + return IOColors[color]; +}; diff --git a/ts/utils/hooks/useBiometricType.ts b/ts/utils/hooks/useBiometricType.ts new file mode 100644 index 00000000000..764a49f599f --- /dev/null +++ b/ts/utils/hooks/useBiometricType.ts @@ -0,0 +1,26 @@ +import * as React from "react"; +import { BiometricsValidType } from "@pagopa/io-app-design-system"; +import { useIOSelector } from "../../store/hooks"; +import { isFingerprintEnabledSelector } from "../../store/reducers/persistedPreferences"; +import { getBiometricsType, isBiometricsValidType } from "../biometrics"; + +export const useBiometricType = () => { + const isFingerprintEnabled = useIOSelector(isFingerprintEnabledSelector); + + const [biometricType, setBiometricType] = React.useState< + BiometricsValidType | undefined + >(undefined); + React.useEffect(() => { + if (isFingerprintEnabled) { + getBiometricsType().then( + biometricsType => + setBiometricType( + isBiometricsValidType(biometricsType) ? biometricsType : undefined + ), + _ => 0 + ); + } + }, [isFingerprintEnabled]); + + return { biometricType, isFingerprintEnabled }; +}; diff --git a/ts/utils/identification/index.tsx b/ts/utils/identification/index.tsx new file mode 100644 index 00000000000..1bcf000cf4a --- /dev/null +++ b/ts/utils/identification/index.tsx @@ -0,0 +1,159 @@ +import * as React from "react"; +import { + BiometricsValidType, + IOStyles, + LabelSmall +} from "@pagopa/io-app-design-system"; +import { View } from "react-native"; +import I18n from "../../i18n"; + +export const FAIL_ATTEMPTS_TO_SHOW_ALERT = 4; + +export const getBiometryIconName = ( + biometryPrintableSimpleType: BiometricsValidType +) => { + switch (biometryPrintableSimpleType) { + case "BIOMETRICS": + case "TOUCH_ID": + return I18n.t("identification.unlockCode.accessibility.fingerprint"); + case "FACE_ID": + return I18n.t("identification.unlockCode.accessibility.faceId"); + } +}; + +const getTranlations = () => { + // We need a function to handle the translations when the language changes, + // or is differnt between the device and the app + const unlockCode = I18n.t("identification.instructions.unlockCode"); + const unlockCodePrefix = I18n.t( + "identification.instructions.unlockCodepPrefix" + ); + const fingerprint = I18n.t("identification.instructions.fingerprint"); + const fingerprintPrefix = I18n.t( + "identification.instructions.fingerprintPrefix" + ); + const faceId = I18n.t("identification.instructions.faceId"); + const faceIdPrefix = I18n.t("identification.instructions.faceIdPrefix"); + return { + unlockCode, + unlockCodePrefix, + fingerprint, + fingerprintPrefix, + faceId, + faceIdPrefix, + congiunction: I18n.t("identification.instructions.congiunction"), + unlockCodeInstruction: `${unlockCodePrefix} ${unlockCode}`, + fingerprintInstruction: `${fingerprintPrefix} ${fingerprint}`, + faceIdInstruction: `${faceIdPrefix} ${faceId}` + }; +}; + +export const getAccessibiliyIdentificationInstructions = ( + biometricType: BiometricsValidType | undefined, + isBimoetricIdentificatoinFailed: boolean = false +) => { + const { + unlockCodeInstruction, + fingerprintInstruction, + faceIdInstruction, + congiunction + } = getTranlations(); + + if (isBimoetricIdentificatoinFailed) { + return unlockCodeInstruction; + } + + switch (biometricType) { + case "BIOMETRICS": + case "TOUCH_ID": + return `${fingerprintInstruction} ${congiunction} ${unlockCodeInstruction}`; + case "FACE_ID": + return `${faceIdInstruction} ${congiunction} ${unlockCodeInstruction}`; + default: + return unlockCodeInstruction; + } +}; + +export const IdentificationInstructionsComponent = (props: { + biometricType: BiometricsValidType | undefined; + isBimoetricIdentificatoinFailed: boolean; +}) => { + const { biometricType, isBimoetricIdentificatoinFailed } = props; + const a11yInstruction = getAccessibiliyIdentificationInstructions( + biometricType, + isBimoetricIdentificatoinFailed + ); + const { + unlockCode, + unlockCodePrefix, + fingerprint, + fingerprintPrefix, + faceId, + faceIdPrefix, + congiunction + } = getTranlations(); + const instructionComponent = ( + + + {unlockCodePrefix} + + {unlockCode} + + ); + const instructionComponentWithFingerprint = ( + + + {fingerprintPrefix} + + {fingerprint} + + ); + const instructionComponentWithFaceId = ( + + + {faceIdPrefix} + + {faceId} + + ); + + if (isBimoetricIdentificatoinFailed) { + return instructionComponent; + } + + switch (biometricType) { + case "BIOMETRICS": + case "TOUCH_ID": + return ( + + {instructionComponentWithFingerprint} + + {" "} + {congiunction}{" "} + + {instructionComponent} + + ); + case "FACE_ID": + return ( + + {instructionComponentWithFaceId} + + {" "} + {congiunction}{" "} + + {instructionComponent} + + ); + default: + return instructionComponent; + } +}; diff --git a/ts/utils/internalLink.ts b/ts/utils/internalLink.ts index 8c1f9d2ba88..70cc9483d7a 100644 --- a/ts/utils/internalLink.ts +++ b/ts/utils/internalLink.ts @@ -16,6 +16,7 @@ import FIMS_ROUTES from "../features/fims/navigation/routes"; import UADONATION_ROUTES from "../features/uaDonations/navigation/routes"; import ROUTES from "../navigation/routes"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; +import { SERVICES_ROUTES } from "../features/services/navigation/routes"; import { isTestEnv } from "./environment"; import { IO_FIMS_LINK_PREFIX, @@ -37,7 +38,7 @@ const routesToNavigationLink: Record = { [MESSAGES_ROUTES.MESSAGES_HOME]: "/main/messages", [ROUTES.PROFILE_PREFERENCES_HOME]: "/profile/preferences", [ROUTES.WALLET_HOME]: "/main/wallet", - [ROUTES.SERVICES_HOME]: "/main/services", + [SERVICES_ROUTES.SERVICES_HOME]: "/main/services", [ROUTES.PROFILE_MAIN]: "/main/profile", [ROUTES.PROFILE_PRIVACY]: "/profile/privacy", [ROUTES.PROFILE_PRIVACY_MAIN]: "/profile/privacy-main", @@ -57,7 +58,7 @@ const cgnRoutesToNavigationLink: Record = { }; const myPortalRoutesToNavigationLink: Record = { - [ROUTES.SERVICE_WEBVIEW]: "/services/webview" + [SERVICES_ROUTES.SERVICE_WEBVIEW]: "/services/webview" }; const uaDonationsRoutesToNavigationLink: Record = { diff --git a/ts/utils/supportAssistance.ts b/ts/utils/supportAssistance.ts index ab446628975..26dd393f941 100644 --- a/ts/utils/supportAssistance.ts +++ b/ts/utils/supportAssistance.ts @@ -1,4 +1,4 @@ -import ZendDesk from "@pagopa/io-react-native-zendesk"; +import * as ZendDesk from "@pagopa/io-react-native-zendesk"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { ToolEnum } from "../../definitions/content/AssistanceToolConfig"; @@ -144,13 +144,10 @@ export const resetAssistanceData = () => { export const hasSubCategories = (zendeskCategory: ZendeskCategory): boolean => (zendeskCategory.zendeskSubCategories?.subCategories ?? []).length > 0; // help can be shown only when remote FF is zendesk + local FF + emailValidated -export const canShowHelp = ( - assistanceTool: ToolEnum, - isEmailValidated: boolean -): boolean => { +export const canShowHelp = (assistanceTool: ToolEnum): boolean => { switch (assistanceTool) { case ToolEnum.zendesk: - return zendeskEnabled && isEmailValidated; + return zendeskEnabled; case ToolEnum.instabug: case ToolEnum.web: case ToolEnum.none: diff --git a/yarn.lock b/yarn.lock index 063bfb61df9..6290c5b3395 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,14 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" @@ -84,6 +92,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" @@ -104,6 +120,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/core@7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" @@ -190,7 +211,7 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/core@^7.14.0", "@babel/core@^7.15.0": +"@babel/core@^7.14.0": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe" integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw== @@ -211,6 +232,27 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/core@^7.18.8", "@babel/core@^7.20.0": + version "7.23.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.7.tgz#4d8016e06a14b5f92530a13ed0561730b5c6483f" + integrity sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.7" + "@babel/parser" "^7.23.6" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.7" + "@babel/types" "^7.23.6" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.4.5": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" @@ -286,6 +328,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -373,6 +425,17 @@ lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" @@ -468,6 +531,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" @@ -527,6 +595,14 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -576,6 +652,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" @@ -639,6 +722,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" @@ -709,6 +799,17 @@ "@babel/traverse" "^7.20.10" "@babel/types" "^7.20.7" +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -757,6 +858,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-regex@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" @@ -870,6 +976,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -926,11 +1039,23 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.18.10", "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" @@ -956,6 +1081,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" @@ -971,6 +1101,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -1046,6 +1181,15 @@ "@babel/traverse" "^7.20.13" "@babel/types" "^7.20.7" +"@babel/helpers@^7.23.7": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.8.tgz#fc6b2d65b16847fd50adddbd4232c76378959e34" + integrity sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.7" + "@babel/types" "^7.23.6" + "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -1082,6 +1226,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.0": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" @@ -1112,6 +1265,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== + "@babel/parser@^7.9.4": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" @@ -1365,6 +1523,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-flow@^7.18.0": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz#084564e0f3cc21ea6c70c44cff984a1c0509729a" + integrity sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-flow@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" @@ -2255,6 +2420,15 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.7.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" @@ -2333,6 +2507,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.23.7": + version "7.23.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.6" + "@babel/types" "^7.23.6" + debug "^4.3.1" + globals "^11.1.0" + "@babel/traverse@^7.7.4": version "7.14.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.7.tgz#64007c9774cfdc3abd23b0780bc18a3ce3631753" @@ -2400,6 +2590,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2857,6 +3056,15 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" @@ -2871,6 +3079,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -2881,6 +3094,19 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -3051,10 +3277,10 @@ dependencies: "@types/node" ">= 8" -"@pagopa/io-app-design-system@1.20.1": - version "1.20.1" - resolved "https://registry.yarnpkg.com/@pagopa/io-app-design-system/-/io-app-design-system-1.20.1.tgz#e439ad96e5dc73eeece2172f5141abfbcb30558e" - integrity sha512-ClSiEj5BrJpTMfR95wrhLz9FkFmitbZivPdEj2VRgi3Xusoclv8lb2RBUQetpPsVTK0cEV4uWPlthSJG/iNbiA== +"@pagopa/io-app-design-system@1.33.0": + version "1.33.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-app-design-system/-/io-app-design-system-1.33.0.tgz#eab0591ed04458f5adb45e358a69154cb6dd164f" + integrity sha512-1wFFGP0xRsjt+OULMtLR/Hk0t5Jj0IfvbshKDbmiY++wlMVUfjKan/s0/gJwe+lL+5x0L1X8Ibw+ynJ03iuzDA== dependencies: "@pagopa/ts-commons" "^12.0.0" "@testing-library/jest-native" "^5.4.2" @@ -3062,12 +3288,13 @@ "@types/react-test-renderer" "^18.0.0" auto-changelog "^2.4.0" lodash "^4.17.21" + react-native-easing-gradient "^1.1.1" react-native-gesture-handler "^2.12.0" react-native-haptic-feedback "^2.0.2" react-native-linear-gradient "^2.5.6" react-native-reanimated "^2.9.1" react-native-safe-area-context "^3.3.2" - react-native-svg "^12.3.0" + react-native-svg "^15.1.0" rn-placeholder "1.3.3" "@pagopa/io-pagopa-commons@^3.1.0": @@ -3079,20 +3306,27 @@ fp-ts "^2.12.1" io-ts "^2.2.16" -"@pagopa/io-react-native-crypto@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-crypto/-/io-react-native-crypto-0.2.1.tgz#3d62b0f0cf45b2a878e4ee0652239ea69089988e" - integrity sha512-J+VP1kLXl1lQSJJYFMa+ljW9fWgMIYQskBJZFaVKPrtrZr8MJyrhNzlFUf2/EGwxm3kA+7o7budq6fPqdfVvvg== +"@pagopa/io-react-native-crypto@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-crypto/-/io-react-native-crypto-0.3.0.tgz#4181a53e36d4cd142b93ef133d3d227d9d360f96" + integrity sha512-3H5CqJwpEYX14QNUhqbSizJBFBeIQRFv8Bw/46+YILTZpvCloVDjfSrPDsKNLZdC1d05wfUSTGGRncMQOB2kMg== -"@pagopa/io-react-native-login-utils@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-login-utils/-/io-react-native-login-utils-0.2.2.tgz#9b37787f98d94fac645e70bda926638b14e59d0d" - integrity sha512-rOChvrMsJ5QvfLvcsttGP6M5LZkOWhOD2YVRL4+bMCVqNRMsSDmteBNoMWzWlRcXrKyd2ZORgDEa1zPmMRMhTw== +"@pagopa/io-react-native-http-client@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-http-client/-/io-react-native-http-client-0.1.3.tgz#a02251edc0cfbbd21525ef950dd5e60a49e01200" + integrity sha512-YLYUgzyIfzWFA2jnUfkUVsCRfUS8qLbcXgl14MbodibpR2IHAkVJBpDbShExUxoTrj6+FQQITpFbIiLZDxCrLQ== + dependencies: + auto-changelog "^2.4.0" -"@pagopa/io-react-native-zendesk@^0.3.28": - version "0.3.28" - resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-zendesk/-/io-react-native-zendesk-0.3.28.tgz#eaef19df577bee000b615f6813d52546a1f2513f" - integrity sha512-U6avcojN+lbQgus1aYiggJ19wsqoHcVCm1EN4Dc7E+f32aCCfvsclFSisKd5FpubGsUdEd/x3De/+5At5c+K9w== +"@pagopa/io-react-native-login-utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-login-utils/-/io-react-native-login-utils-1.0.0.tgz#26aa8fe2ae2c30f18d2714264524fd281f0f2b12" + integrity sha512-9OXV4ebpZAGdFfbV5HOt0YbVhGvVb104KshCA7u74YnsTkJp32FhdIVwoqmerRwNO4rRjwwvDjWZdhT80IrOzA== + +"@pagopa/io-react-native-zendesk@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-zendesk/-/io-react-native-zendesk-0.3.29.tgz#ada8dab8a9ef15e126275baace2084491148bac8" + integrity sha512-CskFyF0Sz+EC/ZyJNNMFEX/Avjyn6cKRwE2K+XSTGWDKYLImPraA8YozTljclCdv3DOxk3ZklUsieMG25PfnSw== "@pagopa/openapi-codegen-ts@^12.2.1": version "12.2.1" @@ -3109,10 +3343,10 @@ write-yaml-file "^4.1.3" yargs "^15.0.1" -"@pagopa/react-native-cie@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@pagopa/react-native-cie/-/react-native-cie-1.2.0.tgz#7c664e810dfb4ea4e86c2a1a9e7c92ce481781a2" - integrity sha512-Dge9dKfsyqNvdc6cn/kEKoAGRQtvsVkO7zNz/oK/CgBGnw9V0h0A1PlogjvN1gsFpGMVuCOC3UiZocxMK95/0A== +"@pagopa/react-native-cie@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@pagopa/react-native-cie/-/react-native-cie-1.2.1.tgz#f38ca7c714c4943da839d232a4c076cc78f0d833" + integrity sha512-6v5DAzy5aTTBag+1idSyClTWRBhZ66kPoh5WXGxZsbKQQSolWhcTyl7pmWDeVG69UnkhIuy8ggXLGQnkrfOSOw== "@pagopa/ts-commons@^10.15.0": version "10.15.0" @@ -3200,42 +3434,42 @@ invariant "^2.2.4" prop-types "^15.7.2" -"@react-native-community/cli-clean@^8.0.4": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-8.0.4.tgz#97e16a20e207b95de12e29b03816e8f2b2c80cc7" - integrity sha512-IwS1M1NHg6+qL8PThZYMSIMYbZ6Zbx+lIck9PLBskbosFo24M3lCOflOl++Bggjakp6mR+sRXxLMexid/GeOsQ== +"@react-native-community/cli-clean@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-9.2.1.tgz#198c5dd39c432efb5374582073065ff75d67d018" + integrity sha512-dyNWFrqRe31UEvNO+OFWmQ4hmqA07bR9Ief/6NnGwx67IO9q83D5PEAf/o96ML6jhSbDwCmpPKhPwwBbsyM3mQ== dependencies: - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" execa "^1.0.0" prompts "^2.4.0" -"@react-native-community/cli-config@^8.0.6": - version "8.0.6" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-8.0.6.tgz#041eee7dd8fdef595bf7a3f24228c173bf294a44" - integrity sha512-mjVpVvdh8AviiO8xtqeX+BkjqE//NMDnISwsLWSJUfNCwTAPmdR8PGbhgP5O4hWHyJ3WkepTopl0ya7Tfi3ifw== +"@react-native-community/cli-config@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-9.2.1.tgz#54eb026d53621ccf3a9df8b189ac24f6e56b8750" + integrity sha512-gHJlBBXUgDN9vrr3aWkRqnYrPXZLztBDQoY97Mm5Yo6MidsEpYo2JIP6FH4N/N2p1TdjxJL4EFtdd/mBpiR2MQ== dependencies: - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-tools" "^9.2.1" cosmiconfig "^5.1.0" deepmerge "^3.2.0" glob "^7.1.3" joi "^17.2.1" -"@react-native-community/cli-debugger-ui@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-8.0.0.tgz#98263dc525e65015e2d6392c940114028f87e8e9" - integrity sha512-u2jq06GZwZ9sRERzd9FIgpW6yv4YOW4zz7Ym/B8eSzviLmy3yI/8mxJtvlGW+J8lBsfMcQoqJpqI6Rl1nZy9yQ== +"@react-native-community/cli-debugger-ui@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-9.0.0.tgz#ea5c5dad6008bccd840d858e160d42bb2ced8793" + integrity sha512-7hH05ZwU9Tp0yS6xJW0bqcZPVt0YCK7gwj7gnRu1jDNN2kughf6Lg0Ys29rAvtZ7VO1PK5c1O+zs7yFnylQDUA== dependencies: serve-static "^1.13.1" -"@react-native-community/cli-doctor@^8.0.6": - version "8.0.6" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-8.0.6.tgz#954250155ab2f3a66a54821e071bc4a631d2dfff" - integrity sha512-ZQqyT9mJMVeFEVIwj8rbDYGCA2xXjJfsQjWk2iTRZ1CFHfhPSUuUiG8r6mJmTinAP9t+wYcbbIYzNgdSUKnDMw== +"@react-native-community/cli-doctor@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-9.3.0.tgz#8817a3fd564453467def5b5bc8aecdc4205eff50" + integrity sha512-/fiuG2eDGC2/OrXMOWI5ifq4X1gdYTQhvW2m0TT5Lk1LuFiZsbTCp1lR+XILKekuTvmYNjEGdVpeDpdIWlXdEA== dependencies: - "@react-native-community/cli-config" "^8.0.6" - "@react-native-community/cli-platform-ios" "^8.0.6" - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-config" "^9.2.1" + "@react-native-community/cli-platform-ios" "^9.3.0" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" command-exists "^1.2.8" envinfo "^7.7.2" @@ -3250,69 +3484,64 @@ sudo-prompt "^9.0.0" wcwidth "^1.0.1" -"@react-native-community/cli-hermes@^8.0.5": - version "8.0.5" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-8.0.5.tgz#639edc6b0ce73f705e4b737e3de1cc47d42516ff" - integrity sha512-Zm0wM6SfgYAEX1kfJ1QBvTayabvh79GzmjHyuSnEROVNPbl4PeCG4WFbwy489tGwOP9Qx9fMT5tRIFCD8bp6/g== +"@react-native-community/cli-hermes@^9.3.4": + version "9.3.4" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-9.3.4.tgz#47851847c4990272687883bd8bf53733d5f3c341" + integrity sha512-VqTPA7kknCXgtYlRf+sDWW4yxZ6Gtg1Ga+Rdrn1qSKuo09iJ8YKPoQYOu5nqbIYJQAEhorWQyo1VvNgd0wd49w== dependencies: - "@react-native-community/cli-platform-android" "^8.0.5" - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-platform-android" "^9.3.4" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" hermes-profile-transformer "^0.0.6" ip "^1.1.5" -"@react-native-community/cli-platform-android@^8.0.4", "@react-native-community/cli-platform-android@^8.0.5": - version "8.0.5" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-8.0.5.tgz#da11d2678adeca98e83494d68de80e50571b4af4" - integrity sha512-z1YNE4T1lG5o9acoQR1GBvf7mq6Tzayqo/za5sHVSOJAC9SZOuVN/gg/nkBa9a8n5U7qOMFXfwhTMNqA474gXA== +"@react-native-community/cli-platform-android@9.3.4", "@react-native-community/cli-platform-android@^9.3.4": + version "9.3.4" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-9.3.4.tgz#42f22943b6ee15713add6af8608c1d0ebf79d774" + integrity sha512-BTKmTMYFuWtMqimFQJfhRyhIWw1m+5N5svR1S5+DqPcyFuSXrpNYDWNSFR8E105xUbFANmsCZZQh6n1WlwMpOA== dependencies: - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" execa "^1.0.0" fs-extra "^8.1.0" glob "^7.1.3" - jetifier "^1.6.2" - lodash "^4.17.15" logkitty "^0.7.1" slash "^3.0.0" -"@react-native-community/cli-platform-ios@^8.0.4", "@react-native-community/cli-platform-ios@^8.0.6": - version "8.0.6" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-8.0.6.tgz#ab80cd4eb3014b8fcfc9bd1b53ec0a9f8e5d1430" - integrity sha512-CMR6mu/LVx6JVfQRDL9uULsMirJT633bODn+IrYmrwSz250pnhON16We8eLPzxOZHyDjm7JPuSgHG3a/BPiRuQ== +"@react-native-community/cli-platform-ios@9.3.0", "@react-native-community/cli-platform-ios@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-9.3.0.tgz#45abde2a395fddd7cf71e8b746c1dc1ee2260f9a" + integrity sha512-nihTX53BhF2Q8p4B67oG3RGe1XwggoGBrMb6vXdcu2aN0WeXJOXdBLgR900DAA1O8g7oy1Sudu6we+JsVTKnjw== dependencies: - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" execa "^1.0.0" glob "^7.1.3" - js-yaml "^3.13.1" - lodash "^4.17.15" ora "^5.4.1" - plist "^3.0.2" -"@react-native-community/cli-plugin-metro@^8.0.4": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-8.0.4.tgz#a364a50a2e05fc5d0b548759e499e5b681b6e4cc" - integrity sha512-UWzY1eMcEr/6262R2+d0Is5M3L/7Y/xXSDIFMoc5Rv5Wucl3hJM/TxHXmByvHpuJf6fJAfqOskyt4bZCvbI+wQ== +"@react-native-community/cli-plugin-metro@^9.3.3": + version "9.3.3" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-9.3.3.tgz#330d7b9476a3fdabdd5863f114fa962289e280dc" + integrity sha512-lPBw6XieNdj2AbWDN0Rc+jNOx8hBgSQyv0gUAm01qtJe4I9FjSMU6nOGTxMpWpICo6TYl/cmPGXOzbfpwxwtkQ== dependencies: - "@react-native-community/cli-server-api" "^8.0.4" - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-server-api" "^9.2.1" + "@react-native-community/cli-tools" "^9.2.1" chalk "^4.1.2" - metro "^0.70.1" - metro-config "^0.70.1" - metro-core "^0.70.1" - metro-react-native-babel-transformer "^0.70.1" - metro-resolver "^0.70.1" - metro-runtime "^0.70.1" + metro "0.72.4" + metro-config "0.72.4" + metro-core "0.72.4" + metro-react-native-babel-transformer "0.72.4" + metro-resolver "0.72.4" + metro-runtime "0.72.4" readline "^1.3.0" -"@react-native-community/cli-server-api@^8.0.4": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-8.0.4.tgz#d45d895a0a6e8b960c9d677188d414a996faa4d3" - integrity sha512-Orr14njx1E70CVrUA8bFdl+mrnbuXUjf1Rhhm0RxUadFpvkHuOi5dh8Bryj2MKtf8eZrpEwZ7tuQPhJEULW16A== +"@react-native-community/cli-server-api@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-9.2.1.tgz#41ac5916b21d324bccef447f75600c03b2f54fbe" + integrity sha512-EI+9MUxEbWBQhWw2PkhejXfkcRqPl+58+whlXJvKHiiUd7oVbewFs0uLW0yZffUutt4FGx6Uh88JWEgwOzAdkw== dependencies: - "@react-native-community/cli-debugger-ui" "^8.0.0" - "@react-native-community/cli-tools" "^8.0.4" + "@react-native-community/cli-debugger-ui" "^9.0.0" + "@react-native-community/cli-tools" "^9.2.1" compression "^1.7.1" connect "^3.6.5" errorhandler "^1.5.0" @@ -3321,15 +3550,14 @@ serve-static "^1.13.1" ws "^7.5.1" -"@react-native-community/cli-tools@^8.0.4": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-8.0.4.tgz#994b9d56c84472491c876b71acd4356773fcbe65" - integrity sha512-ePN9lGxh6LRFiotyddEkSmuqpQhnq2iw9oiXYr4EFWpIEy0yCigTuSTiDF68+c8M9B+7bTwkRpz/rMPC4ViO5Q== +"@react-native-community/cli-tools@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-9.2.1.tgz#c332324b1ea99f9efdc3643649bce968aa98191c" + integrity sha512-bHmL/wrKmBphz25eMtoJQgwwmeCylbPxqFJnFSbkqJPXQz3ManQ6q/gVVMqFyz7D3v+riaus/VXz3sEDa97uiQ== dependencies: appdirsjs "^1.2.4" chalk "^4.1.2" find-up "^5.0.0" - lodash "^4.17.15" mime "^2.4.1" node-fetch "^2.6.0" open "^6.2.0" @@ -3337,40 +3565,37 @@ semver "^6.3.0" shell-quote "^1.7.3" -"@react-native-community/cli-types@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-8.0.0.tgz#72d24178e5ed1c2d271da43e0a4a4f59178f261a" - integrity sha512-1lZS1PEvMlFaN3Se1ksyoFWzMjk+YfKi490GgsqKJln9gvFm8tqVPdnXttI5Uf2DQf3BMse8Bk8dNH4oV6Ewow== +"@react-native-community/cli-types@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-9.1.0.tgz#dcd6a0022f62790fe1f67417f4690db938746aab" + integrity sha512-KDybF9XHvafLEILsbiKwz5Iobd+gxRaPyn4zSaAerBxedug4er5VUWa8Szy+2GeYKZzMh/gsb1o9lCToUwdT/g== dependencies: joi "^17.2.1" -"@react-native-community/cli@^8.0.4": - version "8.0.6" - resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-8.0.6.tgz#7aae37843ab8e44b75c477c1de69f4c902e599ef" - integrity sha512-E36hU/if3quQCfJHGWVkpsCnwtByRCwORuAX0r6yr1ebKktpKeEO49zY9PAu/Z1gfyxCtgluXY0HfRxjKRFXTg== - dependencies: - "@react-native-community/cli-clean" "^8.0.4" - "@react-native-community/cli-config" "^8.0.6" - "@react-native-community/cli-debugger-ui" "^8.0.0" - "@react-native-community/cli-doctor" "^8.0.6" - "@react-native-community/cli-hermes" "^8.0.5" - "@react-native-community/cli-plugin-metro" "^8.0.4" - "@react-native-community/cli-server-api" "^8.0.4" - "@react-native-community/cli-tools" "^8.0.4" - "@react-native-community/cli-types" "^8.0.0" +"@react-native-community/cli@9.3.5": + version "9.3.5" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-9.3.5.tgz#73626d3be8f5e2e6389f2555d126666fb8de4389" + integrity sha512-X+/xSysHsb0rXUWZKtXnKGhUNMRPxYzyhBc3VMld+ygPaFG57TAdK9rFGRu7NkIsRI6qffF/SukQPVlBZIfBHg== + dependencies: + "@react-native-community/cli-clean" "^9.2.1" + "@react-native-community/cli-config" "^9.2.1" + "@react-native-community/cli-debugger-ui" "^9.0.0" + "@react-native-community/cli-doctor" "^9.3.0" + "@react-native-community/cli-hermes" "^9.3.4" + "@react-native-community/cli-plugin-metro" "^9.3.3" + "@react-native-community/cli-server-api" "^9.2.1" + "@react-native-community/cli-tools" "^9.2.1" + "@react-native-community/cli-types" "^9.1.0" chalk "^4.1.2" - commander "^2.19.0" + commander "^9.4.0" execa "^1.0.0" find-up "^4.1.0" fs-extra "^8.1.0" graceful-fs "^4.1.3" - leven "^3.1.0" - lodash "^4.17.15" - minimist "^1.2.0" prompts "^2.4.0" semver "^6.3.0" -"@react-native-community/datetimepicker@^3.0.2", "@react-native-community/datetimepicker@^3.5.2": +"@react-native-community/datetimepicker@^3.0.2": version "3.5.2" resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-3.5.2.tgz#8e6feb30f2620e5abdf565d5fe74c0c04edcf6ae" integrity sha512-TWRuAtr/DnrEcRewqvXMLea2oB+YF+SbtuYLHguALLxNJQLl/RFB7aTNZeF+OoH75zKFqtXECXV1/uxQUpA+sg== @@ -3425,11 +3650,6 @@ resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-1.16.8.tgz#2126ca54d4a5a3e9ea5e3f39ad1e6643f8e4b3d4" integrity sha512-pacdQDX6V6EmjF+HoiIh6u++qx4mTK0WnhgUHRc01B+Qt5eoeUwseBqmqfTSXTx/aHDEd6PiIw7UGvKgFoqgFQ== -"@react-native-picker/picker@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-2.4.1.tgz#92feb7e0672d739624517dae04bf4de1452dfcdc" - integrity sha512-1XWy3IQgwr7MWd30KdY1iUh2gQZD+JiotN1ifj/ptFUYKon/0UFwngKQaWCO/CP/FdLl20/huSSLwKedYrdMMA== - "@react-native/assets@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" @@ -3445,55 +3665,73 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@react-navigation/bottom-tabs@^5.11.15": - version "5.11.15" - resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.15.tgz#f973625cc32d9c5a4067851f084cb11ccd68fe79" - integrity sha512-TBY419W6aN/HZg98xbVp5Bx1HEF5sXuHR5f55W6KMI4k2AvxlwelKD1wbfvEcX2iuQT0YUiiXsACRFUSECYhkw== +"@react-navigation/bottom-tabs@6.5.11": + version "6.5.11" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.11.tgz#b6e67a3aa19e60ed9c1139fa0253586c479832d5" + integrity sha512-CBN/NOdxnMvmjw+AJQI1kltOYaClTZmGec5pQ3ZNTPX86ytbIOylDIITKMfTgHZcIEFQDymx1SHeS++PIL3Szw== dependencies: - color "^3.1.3" - react-native-iphone-x-helper "^1.3.0" + "@react-navigation/elements" "^1.3.21" + color "^4.2.3" + warn-once "^0.1.0" -"@react-navigation/core@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-5.16.1.tgz#e0d308bd9bbd930114ce55c4151806b6d7907f69" - integrity sha512-3AToC7vPNeSNcHFLd1h71L6u34hfXoRAS1CxF9Fc4uC8uOrVqcNvphpeFbE0O9Bw6Zpl0BnMFl7E5gaL3KGzNA== +"@react-navigation/core@^6.4.10": + version "6.4.10" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.10.tgz#0c52621968b35e3a75e189e823d3b9e3bad77aff" + integrity sha512-oYhqxETRHNHKsipm/BtGL0LI43Hs2VSFoWMbBdHK9OqgQPjTVUitslgLcPpo4zApCcmBWoOLX2qPxhsBda644A== dependencies: - "@react-navigation/routers" "^5.7.4" + "@react-navigation/routers" "^6.1.9" escape-string-regexp "^4.0.0" - nanoid "^3.1.15" - query-string "^6.13.6" + nanoid "^3.1.23" + query-string "^7.1.3" react-is "^16.13.0" + use-latest-callback "^0.1.7" + +"@react-navigation/elements@^1.3.21": + version "1.3.21" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.21.tgz#debac6becc6b6692da09ec30e705e476a780dfe1" + integrity sha512-eyS2C6McNR8ihUoYfc166O1D8VYVh9KIl0UQPI8/ZJVsStlfSTgeEEh+WXge6+7SFPnZ4ewzEJdSAHH+jzcEfg== + +"@react-navigation/material-top-tabs@6.6.5": + version "6.6.5" + resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.5.tgz#5cfc33e0d02f2dcd1a0654284704f4eef1d16697" + integrity sha512-ovKc+ltWYJwu3ju5sw1txBTMemlRM85/JceSrkqU++QnL9l0TAPiPxDlO+wJddR1iwi+P6zj5/+QkXR5Ku+trw== + dependencies: + color "^4.2.3" + warn-once "^0.1.0" -"@react-navigation/material-top-tabs@^5.x": - version "5.3.19" - resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-5.3.19.tgz#64f3a933f5d7e86e99f3d57d9f0c1e833ffa7e4f" - integrity sha512-I7bEF99THxxcY7kCUZ5pPmwXr6kgo6L2sg3P1YJo+CcBWSGvGiHyNbZXNs15HuKRuFvEuueChNV9n8QuKBWbDA== +"@react-navigation/native-stack@^6.9.17": + version "6.9.17" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.9.17.tgz#4fc370b14be07296423ae8c00940fb002c6001b5" + integrity sha512-X8p8aS7JptQq7uZZNFEvfEcPf6tlK4PyVwYDdryRbG98B4bh2wFQYMThxvqa+FGEN7USEuHdv2mF0GhFKfX0ew== dependencies: - color "^3.1.3" + "@react-navigation/elements" "^1.3.21" + warn-once "^0.1.0" -"@react-navigation/native@^5.9.8": - version "5.9.8" - resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-5.9.8.tgz#ac76ee6390ea7ce807486ca5c38d903e23433a97" - integrity sha512-DNbcDHXQPSFDLn51kkVVJjT3V7jJy2GztNYZe/2bEg29mi5QEcHHcpifjMCtyFKntAOWzKlG88UicIQ17UEghg== +"@react-navigation/native@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.1.9.tgz#8ef87095cd9c2ed094308c726157c7f6fc28796e" + integrity sha512-AMuJDpwXE7UlfyhIXaUCCynXmv69Kb8NzKgKJO7v0k0L+u6xUTbt6xvshmJ79vsvaFyaEH9Jg5FMzek5/S5qNw== dependencies: - "@react-navigation/core" "^5.16.1" + "@react-navigation/core" "^6.4.10" escape-string-regexp "^4.0.0" - nanoid "^3.1.15" + fast-deep-equal "^3.1.3" + nanoid "^3.1.23" -"@react-navigation/routers@^5.7.4": - version "5.7.4" - resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-5.7.4.tgz#8b5460e841a0c64f6c9a5fbc2a1eb832432d4fb0" - integrity sha512-0N202XAqsU/FlE53Nmh6GHyMtGm7g6TeC93mrFAFJOqGRKznT0/ail+cYlU6tNcPA9AHzZu1Modw1eoDINSliQ== +"@react-navigation/routers@^6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-6.1.9.tgz#73f5481a15a38e36592a0afa13c3c064b9f90bed" + integrity sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA== dependencies: - nanoid "^3.1.15" + nanoid "^3.1.23" -"@react-navigation/stack@^5.14.9": - version "5.14.9" - resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-5.14.9.tgz#49c7b9316e6fb456e9766c901e0d607862f0ea7d" - integrity sha512-DuvrT9P+Tz8ezZLQYxORZqOGqO+vEufaxlW1hSLw1knLD4jNxkz8TJDXtfKwaz//9gb43UhTNccNM02vm7iPqQ== +"@react-navigation/stack@6.3.20": + version "6.3.20" + resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-6.3.20.tgz#8eec944888f317bb1ba1ff30e7f513806bea16c2" + integrity sha512-vE6mgZzOgoa5Uy7ayT97Cj+ZIK7DK+JBYVuKUViILlWZy6IWK7HFDuqoChSbZ1ajTIfAxj/acVGg1jkbAKsToA== dependencies: - color "^3.1.3" - react-native-iphone-x-helper "^1.3.0" + "@react-navigation/elements" "^1.3.21" + color "^4.2.3" + warn-once "^0.1.0" "@redux-saga/core@^1.1.3": version "1.1.3" @@ -4011,10 +4249,10 @@ resolved "https://registry.yarnpkg.com/@types/react-native-push-notification/-/react-native-push-notification-8.1.1.tgz#0c9a181d7823cfad215d040bc5596c2d83e1a3cd" integrity sha512-ZN4UbU4EM3C7XGt4zI6RqHEZS2+35EwOz9DPAD1lTTY3IpWMHAKYjryykvP35hFkSwrGMpT8nYuMFPEJRwDEJA== -"@types/react-native@0.69.6": - version "0.69.6" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.69.6.tgz#b792b7eb024a14869fdbbe97536e6014cb3be731" - integrity sha512-jx1QdJT3CdQc42EpoIGu22F1wrPZjmC/CNkfR5sRs5GxloJzthuICK7CKqAGEo2SekPs+YYzhbzrJGi1IrG5Lg== +"@types/react-native@0.70.19": + version "0.70.19" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.70.19.tgz#b4e651dcf7f49c69ff3a4c3072584cad93155582" + integrity sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg== dependencies: "@types/react" "*" @@ -4776,10 +5014,10 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" - integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== +ast-types@0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" + integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== dependencies: tslib "^2.0.1" @@ -5751,6 +5989,16 @@ browserslist@^4.21.3: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.22.2: + version "4.22.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" + integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== + dependencies: + caniuse-lite "^1.0.30001565" + electron-to-chromium "^1.4.601" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -5986,6 +6234,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== +caniuse-lite@^1.0.30001565: + version "1.0.30001579" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz#45c065216110f46d6274311a4b3fcf6278e0852a" + integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -6282,7 +6535,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3: +color-convert@^1.9.0, color-convert@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -6314,10 +6567,10 @@ color-string@^1.5.2: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-string@^1.6.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" - integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -6330,13 +6583,13 @@ color@^3.0.0, color@~3.1.2: color-convert "^1.9.1" color-string "^1.5.2" -color@^3.1.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" + color-convert "^2.0.1" + color-string "^1.9.0" colorette@^1.0.7: version "1.3.0" @@ -6406,7 +6659,7 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1" integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA== -commander@^2.18.0, commander@^2.19.0, commander@^2.20.3: +commander@^2.18.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -6426,6 +6679,11 @@ commander@^8.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.4.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -6709,6 +6967,11 @@ convert-source-map@^1.7.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -6886,15 +7149,15 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-select@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" - domutils "^2.8.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" nth-check "^2.0.1" css-to-react-native@^3.0.0: @@ -6914,15 +7177,7 @@ css-tree@1.0.0-alpha.37: mdn-data "2.0.4" source-map "^0.6.1" -css-tree@^1.0.0-alpha.39: - version "1.0.0-alpha.39" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" - integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== - dependencies: - mdn-data "2.0.6" - source-map "^0.6.1" - -css-tree@^1.1.2: +css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== @@ -6935,10 +7190,10 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== -css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssfilter@0.0.10: version "0.0.10" @@ -7130,7 +7385,7 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== -decode-uri-component@^0.2.0: +decode-uri-component@^0.2.0, decode-uri-component@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== @@ -7421,6 +7676,15 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -7441,6 +7705,11 @@ domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -7448,13 +7717,20 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.0: +domhandler@^4.2.0, domhandler@^4.2.2: version "4.3.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== dependencies: domelementtype "^2.2.0" +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -7472,6 +7748,15 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" @@ -7548,6 +7833,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz#0e039de59135f44ab9a8ec9025e53a9135eba11f" integrity sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ== +electron-to-chromium@^1.4.601: + version "1.4.639" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.639.tgz#c6f9cc685f9efb2980d2cfc95a27f8142c9adf28" + integrity sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg== + electron-to-chromium@^1.4.84: version "1.4.107" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.107.tgz#564257014ab14033b4403a309c813123c58a3fb9" @@ -7629,6 +7919,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + envinfo@^7.7.2: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -8742,7 +9037,7 @@ fsevents@^2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -9284,22 +9579,17 @@ hasurl@^1.0.0: resolved "https://registry.yarnpkg.com/hasurl/-/hasurl-1.0.0.tgz#e4c619097ae1e8fc906bee904ce47e94f5e1ea37" integrity sha512-43ypUd3DbwyCT01UYpA99AEZxZ4aKtRxWGBHEIbjcOsUghd9YUON0C+JF6isNjaiwC/UF5neaUudy6JS9jZPZQ== -hermes-engine@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/hermes-engine/-/hermes-engine-0.11.0.tgz#bb224730d230a02a5af02c4e090d1f52d57dd3db" - integrity sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw== - -hermes-estree@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.6.0.tgz#e866fddae1b80aec65fe2ae450a5f2070ad54033" - integrity sha512-2YTGzJCkhdmT6VuNprWjXnvTvw/3iPNw804oc7yknvQpNKo+vJGZmtvLLCghOZf0OwzKaNAzeIMp71zQbNl09w== +hermes-estree@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.8.0.tgz#530be27243ca49f008381c1f3e8b18fb26bf9ec0" + integrity sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q== -hermes-parser@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.6.0.tgz#00d14e91bca830b3c1457050fa4187400cb96328" - integrity sha512-Vf58jBZca2+QBLR9h7B7mdg8oFz2g5ILz1iVouZ5DOrOrAfBmPfJjdjDT8jrO0f+iJ4/hSRrQHqHIjSnTaLUDQ== +hermes-parser@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.8.0.tgz#116dceaba32e45b16d6aefb5c4c830eaeba2d257" + integrity sha512-yZKalg1fTYG5eOiToLUaw69rQfZq/fi+/NtEXRU7N87K/XobNRhRWorh80oSge2lWUiZfTgUvRJH+XgZWrhoqA== dependencies: - hermes-estree "0.6.0" + hermes-estree "0.8.0" hermes-profile-transformer@^0.0.6: version "0.0.6" @@ -9640,9 +9930,9 @@ io-ts@^2.2.16: integrity sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q== ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== ipaddr.js@1.9.1: version "1.9.1" @@ -10362,26 +10652,6 @@ jest-haste-map@^26.6.2: optionalDependencies: fsevents "^2.1.2" -jest-haste-map@^27.3.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" - integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== - dependencies: - "@jest/types" "^27.5.1" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^27.5.1" - jest-serializer "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" - micromatch "^4.0.4" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.3.2" - jest-jasmine2@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" @@ -10492,7 +10762,7 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-regex-util@^27.5.1: +jest-regex-util@^27.0.6: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== @@ -10587,7 +10857,7 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-serializer@^27.5.1: +jest-serializer@^27.0.6: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== @@ -10629,7 +10899,7 @@ jest-util@^26.6.2: is-ci "^2.0.0" micromatch "^4.0.2" -jest-util@^27.5.1: +jest-util@^27.2.0: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== @@ -10675,7 +10945,7 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.2.0, jest-worker@^27.5.1: +jest-worker@^27.2.0: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -10693,11 +10963,6 @@ jest@^26.6.3: import-local "^3.0.2" jest-cli "^26.6.3" -jetifier@^1.6.2: - version "1.6.6" - resolved "https://registry.yarnpkg.com/jetifier/-/jetifier-1.6.6.tgz#fec8bff76121444c12dc38d2dad6767c421dab68" - integrity sha512-JNAkmPeB/GS2tCRqUzRPsTOHpGDah7xP18vGJfIjZC+W2sxEHbxgJxetIjIqhjQ3yYbYNEELkM/spKLtwoOSUQ== - joi@^17.2.1: version "17.4.2" resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7" @@ -10761,10 +11026,15 @@ jsc-android@^250230.2.1: resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-250230.2.1.tgz#3790313a970586a03ab0ad47defbc84df54f1b83" integrity sha512-KmxeBlRjwoqCnBBKGsihFtvsBHyUFlBxJPK4FzeYcIuBfdjv6jFys44JITAgSTbQD+vIdwMEfyZklsuQX0yI1Q== -jscodeshift@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.13.1.tgz#69bfe51e54c831296380585c6d9e733512aecdef" - integrity sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ== +jsc-safe-url@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz#141c14fbb43791e88d5dc64e85a374575a83477a" + integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== + +jscodeshift@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.14.0.tgz#7542e6715d6d2e8bde0b4e883f0ccea358b46881" + integrity sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA== dependencies: "@babel/core" "^7.13.16" "@babel/parser" "^7.13.16" @@ -10779,10 +11049,10 @@ jscodeshift@^0.13.1: chalk "^4.1.2" flow-parser "0.*" graceful-fs "^4.2.4" - micromatch "^3.1.10" + micromatch "^4.0.4" neo-async "^2.5.0" node-dir "^0.1.17" - recast "^0.20.4" + recast "^0.21.0" temp "^0.8.4" write-file-atomic "^2.3.0" @@ -10931,7 +11201,7 @@ json5@^2.1.0, json5@^2.1.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -json5@^2.2.1, json5@^2.2.2: +json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -11620,11 +11890,6 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -mdn-data@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" - integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== - mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -11762,69 +12027,88 @@ metro-babel-transformer@0.58.0: "@babel/core" "^7.0.0" metro-source-map "0.58.0" -metro-babel-transformer@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.70.3.tgz#dca61852be273824a4b641bd1ecafff07ff3ad1f" - integrity sha512-bWhZRMn+mIOR/s3BDpFevWScz9sV8FGktVfMlF1eJBLoX24itHDbXvTktKBYi38PWIKcHedh6THSFpJogfuwNA== +metro-babel-transformer@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.72.4.tgz#5149424896797980aa1758c8ef7c9a80f9d0f587" + integrity sha512-cg1TQUKDkKqrIClrqqIGE8ZDa9kRKSjhBtqPtNYt/ZSywXU41SrldfcI5uzPrzcIrYpH5hnN6OCLRACPgy2vsw== dependencies: "@babel/core" "^7.14.0" - hermes-parser "0.6.0" - metro-source-map "0.70.3" + hermes-parser "0.8.0" + metro-source-map "0.72.4" nullthrows "^1.1.1" -metro-cache-key@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.70.3.tgz#898803db04178a8f440598afba7d82a9cf35abf7" - integrity sha512-0zpw+IcpM3hmGd5sKMdxNv3sbOIUYnMUvx1/yaM6vNRReSPmOLX0bP8fYf3CGgk8NEreZ1OHbVsuw7bdKt40Mw== +metro-cache-key@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.72.4.tgz#f03d49214554b25968f04dc5e19dfe018cf9312b" + integrity sha512-DH3cgN4L7IKNCVBy8LBOXQ4tHDdvh7Vl7jWNkQKMOfHWu1EwsTtXD/+zdV7/be4ls/kHxrD0HbGzpK8XhUAHSw== -metro-cache@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.70.3.tgz#42cf3cdf8a7b3691f3bef9a86bed38d4c5f6201f" - integrity sha512-iCix/+z812fUqa6KlOxaTkY6LQQDoXIe/VljXkGIvpygSCmYyhjQpfQVZEVVPezFmUBYXNdabdQ6cYx6JX3yMg== +metro-cache@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.72.4.tgz#e0ffb33dd044a7cf5897a09489088a413bfe7468" + integrity sha512-76fi9OVytiFVSuGQcNoquVOT7AENd0q3n1WmyBeJ7jvl/UrE3/NN3HTWzu2ezG5IxF3cmo5q1ehi0NEpgwaFGg== dependencies: - metro-core "0.70.3" + metro-core "0.72.4" rimraf "^2.5.4" -metro-config@0.70.3, metro-config@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.70.3.tgz#fe6f7330f679d5594e5724af7a69d4dbe1bb5bc3" - integrity sha512-SSCDjSTygoCgzoj61DdrBeJzZDRwQxUEfcgc6t6coxWSExXNR4mOngz0q4SAam49Bmjq9J2Jft6qUKnUTPrRgA== +metro-config@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.72.4.tgz#3ad42b3ca0037125d5615f4cb7e1c7ed9442bedd" + integrity sha512-USv+H14D5RrSpfA5t4t5cbF1CnizgYGz6xJ3HB0r/bDYdJdZTVqB3/mMPft7Z5zHslS00JCG7oE51G1CK/FlKw== dependencies: cosmiconfig "^5.0.5" jest-validate "^26.5.2" - metro "0.70.3" - metro-cache "0.70.3" - metro-core "0.70.3" - metro-runtime "0.70.3" + metro "0.72.4" + metro-cache "0.72.4" + metro-core "0.72.4" + metro-runtime "0.72.4" -metro-core@0.70.3, metro-core@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.70.3.tgz#bf4dda15a5185f5a7931de463a1b97ac9ef680a0" - integrity sha512-NzfHB/w5R7yLaOeU1tzPTbBzCRsYSvpKJkLMP0yudszKZzIAZqNdjoEJ9GZ688Wi0ynZxcU0BxukXh4my80ZBw== +metro-core@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.72.4.tgz#e4939aef4c50d953c44eee99a3c971d5162f1287" + integrity sha512-2JNT1nG0UV1uMrQHQOKUSII0sdS6MhVT3mBt2kwfjCvD+jvi1iYhKJ4kYCRlUQw9XNLGZ/B+C0VDQzlf2M3zVw== dependencies: - jest-haste-map "^27.3.1" lodash.throttle "^4.1.1" - metro-resolver "0.70.3" + metro-resolver "0.72.4" -metro-hermes-compiler@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.70.3.tgz#ac7ed656fbcf0a59adcd010d3639e4cfdbc76b4f" - integrity sha512-W6WttLi4E72JL/NyteQ84uxYOFMibe0PUr9aBKuJxxfCq6QRnJKOVcNY0NLW0He2tneXGk+8ZsNz8c0flEvYqg== +metro-file-map@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.72.4.tgz#8a0c8a0e44d665af90dded2ac6e01baebff8552e" + integrity sha512-Mv5WgTsYs5svTR/df6jhq2aD4IkAuwV5TutHW0BfEg1YccQt8/v7q5ZypmUOkjdSS9bFR4r3677jalr/ceFypQ== + dependencies: + abort-controller "^3.0.0" + anymatch "^3.0.3" + debug "^2.2.0" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-regex-util "^27.0.6" + jest-serializer "^27.0.6" + jest-util "^27.2.0" + jest-worker "^27.2.0" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" -metro-inspector-proxy@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.70.3.tgz#321c25b2261e76d8c4bcc39e092714adfcb50a14" - integrity sha512-qQoNdPGrmyoJSWYkxSDpTaAI8xyqVdNDVVj9KRm1PG8niSuYmrCCFGLLFsMvkVYwsCWUGHoGBx0UoAzVp14ejw== +metro-hermes-compiler@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.72.4.tgz#06c946d74720d5132fa1690df0610ba367d3436c" + integrity sha512-AY1mAT5FKfDRYCthuKo2XHbuhG5TUV4ZpZlJ8peIgkiWICzfy0tau3yu+3jUD456N90CjMCOmdknji4uKiZ8ww== + +metro-inspector-proxy@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.72.4.tgz#347e9634b6204c38117292edfb11eb2df71c09ad" + integrity sha512-pr+PsbNCZaStWuJRH8oclT170B7NxfgH+UUyTf9/aR+7PjX0gdDabJhPyzA633QgR+EFBaQKZuetHA+f5/cnEQ== dependencies: connect "^3.6.5" debug "^2.2.0" ws "^7.5.1" yargs "^15.3.1" -metro-minify-uglify@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.70.3.tgz#2f28129ca5b8ef958f3e3fcf004c3707c7732e1e" - integrity sha512-oHyjV9WDqOlDE1FPtvs6tIjjeY/oP1PNUPYL1wqyYtqvjN+zzAOrcbsAAL1sv+WARaeiMsWkF2bwtNo+Hghoog== +metro-minify-uglify@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.72.4.tgz#b4504adc17f093173c0e5d44df32ac9e13f50a88" + integrity sha512-84Rrgie3O7Dqkak9ep/eIpMZkEFzpKD4bngPUNimYqAMCExKL7/aymydB27gKcqwus/BVkAV+aOnFsuOhlgnQg== dependencies: uglify-es "^3.1.9" @@ -11869,10 +12153,10 @@ metro-react-native-babel-preset@0.58.0: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-preset@0.70.3, metro-react-native-babel-preset@^0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.3.tgz#1c77ec4544ecd5fb6c803e70b21284d7483e4842" - integrity sha512-4Nxc1zEiHEu+GTdEMEsHnRgfaBkg8f/Td3+FcQ8NTSvs+xL3LBrQy6N07idWSQZHIdGFf+tTHvRfSIWLD8u8Tg== +metro-react-native-babel-preset@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.72.4.tgz#2b320772d2489d1fb3a6413fc58dad13a56eea0e" + integrity sha512-YGCVaYe1H5fOFktdDdL9IwAyiXjPh1t2eZZFp3KFJak6fxKpN+q5PPhe1kzMa77dbCAqgImv43zkfGa6i27eyA== dependencies: "@babel/core" "^7.14.0" "@babel/plugin-proposal-async-generator-functions" "^7.0.0" @@ -11914,17 +12198,61 @@ metro-react-native-babel-preset@0.70.3, metro-react-native-babel-preset@^0.70.3: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-transformer@0.70.3, metro-react-native-babel-transformer@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.70.3.tgz#195597c32488f820aa9e441bbca7c04fe7de7a2d" - integrity sha512-WKBU6S/G50j9cfmFM4k4oRYprd8u3qjleD4so1E2zbTNILg+gYla7ZFGCAvi2G0ZcqS2XuGCR375c2hF6VVvwg== +metro-react-native-babel-preset@^0.73.0: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.10.tgz#304b24bb391537d2c987732cc0a9774be227d3f6" + integrity sha512-1/dnH4EHwFb2RKEKx34vVDpUS3urt2WEeR8FYim+ogqALg4sTpG7yeQPxWpbgKATezt4rNfqAANpIyH19MS4BQ== + dependencies: + "@babel/core" "^7.20.0" + "@babel/plugin-proposal-async-generator-functions" "^7.0.0" + "@babel/plugin-proposal-class-properties" "^7.0.0" + "@babel/plugin-proposal-export-default-from" "^7.0.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.0.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-optional-chaining" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.0.0" + "@babel/plugin-syntax-export-default-from" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.18.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-syntax-optional-chaining" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.0.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.0.0" + "@babel/plugin-transform-flow-strip-types" "^7.0.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-template-literals" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.5.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + "@babel/template" "^7.0.0" + react-refresh "^0.4.0" + +metro-react-native-babel-transformer@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.72.4.tgz#c1a38bf28513374dbb0fce45b4017d8abfe4a071" + integrity sha512-VxM8Cki+/tPAyQRPHEy1bsxAihpxz8cGLdteFo9t0eAJI7/vEegqICxQm4A+RiGQc4f8t2jiwI6YpnDWomI5Gw== dependencies: "@babel/core" "^7.14.0" babel-preset-fbjs "^3.4.0" - hermes-parser "0.6.0" - metro-babel-transformer "0.70.3" - metro-react-native-babel-preset "0.70.3" - metro-source-map "0.70.3" + hermes-parser "0.8.0" + metro-babel-transformer "0.72.4" + metro-react-native-babel-preset "0.72.4" + metro-source-map "0.72.4" nullthrows "^1.1.1" metro-react-native-babel-transformer@^0.58.0: @@ -11938,19 +12266,20 @@ metro-react-native-babel-transformer@^0.58.0: metro-react-native-babel-preset "0.58.0" metro-source-map "0.58.0" -metro-resolver@0.70.3, metro-resolver@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.70.3.tgz#c64fdd6d0a88fa62f3f99f87e539b5f603bd47bf" - integrity sha512-5Pc5S/Gs4RlLbziuIWtvtFd9GRoILlaRC8RZDVq5JZWcWHywKy/PjNmOBNhpyvtRlzpJfy/ssIfLhu8zINt1Mw== +metro-resolver@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.72.4.tgz#37893ff72273a2b7ea529564caa15fe2e2337267" + integrity sha512-aHxq/jypzGyi9Ic9woe//RymfxpzWliAkyTmBWPHE9ypGoiobstK0me2j5XuSfzASzCU8wcVt20qy870rxTWLw== dependencies: absolute-path "^0.0.0" -metro-runtime@0.70.3, metro-runtime@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.70.3.tgz#09231b9d05dcbdfb5a13df0a45307273e6fe1168" - integrity sha512-22xU7UdXZacniTIDZgN2EYtmfau2pPyh97Dcs+cWrLcJYgfMKjWBtesnDcUAQy3PHekDYvBdJZkoQUeskYTM+w== +metro-runtime@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.72.4.tgz#b3469fd040a9526bfd897c0517c5f052a059ddeb" + integrity sha512-EA0ltqyYFpjOdpoRqE2U9FJleqTOIK+ZLRlLaDrx4yz3zTqUZ16W6w71dq+qrwD8BPg7bPKQu7RluU3K6tI79A== dependencies: "@babel/runtime" "^7.0.0" + react-refresh "^0.4.0" metro-source-map@0.58.0: version "0.58.0" @@ -11965,17 +12294,17 @@ metro-source-map@0.58.0: source-map "^0.5.6" vlq "^1.0.0" -metro-source-map@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.70.3.tgz#f5976108c18d4661eaa4d188c96713e5d67a903b" - integrity sha512-zsYtZGrwRbbGEFHtmMqqeCH9K9aTGNVPsurMOWCUeQA3VGyVGXPGtLMC+CdAM9jLpUyg6jw2xh0esxi+tYH7Uw== +metro-source-map@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.72.4.tgz#3c6444bba22b84d7d7e383f784a1d59e724192de" + integrity sha512-P09aMDEPkLo6BM8VYYoTsH/2B1w6t+mrCwNcNJV1zE+57FPiU4fSBlSeM8G9YeYaezDTHimS2JlMozP+2r+trA== dependencies: "@babel/traverse" "^7.14.0" "@babel/types" "^7.0.0" invariant "^2.2.4" - metro-symbolicate "0.70.3" + metro-symbolicate "0.72.4" nullthrows "^1.1.1" - ob1 "0.70.3" + ob1 "0.72.4" source-map "^0.5.6" vlq "^1.0.0" @@ -11990,22 +12319,22 @@ metro-symbolicate@0.58.0: through2 "^2.0.1" vlq "^1.0.0" -metro-symbolicate@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.70.3.tgz#b039e5629c4ed0c999ea0496d580e1c98260f5cb" - integrity sha512-JTYkF1dpeDUssQ84juE1ycnhHki2ylJBBdJE1JHtfu5oC+z1ElDbBdPHq90Uvt8HbRov/ZAnxvv7Zy6asS+WCA== +metro-symbolicate@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.72.4.tgz#3be7c9d1f382fc58198efcb515f2de0ec3fc4181" + integrity sha512-6ZRo66Q4iKiwaQuHjmogkSCCqaSpJ4QzbHsVHRUe57mFIL34lOLYp7aPfmX7NHCmy061HhDox/kGuYZQRmHB3A== dependencies: invariant "^2.2.4" - metro-source-map "0.70.3" + metro-source-map "0.72.4" nullthrows "^1.1.1" source-map "^0.5.6" through2 "^2.0.1" vlq "^1.0.0" -metro-transform-plugins@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.70.3.tgz#7fe87cd0d8979b4d5d6e375751d86188fff38fd9" - integrity sha512-dQRIJoTkWZN2IVS2KzgS1hs7ZdHDX3fS3esfifPkqFAEwHiLctCf0EsPgIknp0AjMLvmGWfSLJigdRB/dc0ASw== +metro-transform-plugins@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.72.4.tgz#01e95aa277216fb0887610067125fac9271d399e" + integrity sha512-yxB4v/LxQkmN1rjyyeLiV4x+jwCmId4FTTxNrmTYoi0tFPtOBOeSwuqY08LjxZQMJdZOKXqj2bgIewqFXJEkGw== dependencies: "@babel/core" "^7.14.0" "@babel/generator" "^7.14.0" @@ -12013,29 +12342,29 @@ metro-transform-plugins@0.70.3: "@babel/traverse" "^7.14.0" nullthrows "^1.1.1" -metro-transform-worker@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.70.3.tgz#62bfa28ebef98803531c4bcb558de5fc804c94ef" - integrity sha512-MtVVsnHhhBOp9GRLCdAb2mD1dTCsIzT4+m34KMRdBDCEbDIb90YafT5prpU8qbj5uKd0o2FOQdrJ5iy5zQilHw== +metro-transform-worker@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.72.4.tgz#356903c343dc62373b928b4325ad09a103398cc5" + integrity sha512-mIvzy6nRQKMALEdF5g8LXPgCOUi/tGESE5dlb7OSMCj2FAFBm3mTLRrpW5phzK/J6Wg+4Vb9PMS+wGbXR261rA== dependencies: "@babel/core" "^7.14.0" "@babel/generator" "^7.14.0" "@babel/parser" "^7.14.0" "@babel/types" "^7.0.0" babel-preset-fbjs "^3.4.0" - metro "0.70.3" - metro-babel-transformer "0.70.3" - metro-cache "0.70.3" - metro-cache-key "0.70.3" - metro-hermes-compiler "0.70.3" - metro-source-map "0.70.3" - metro-transform-plugins "0.70.3" + metro "0.72.4" + metro-babel-transformer "0.72.4" + metro-cache "0.72.4" + metro-cache-key "0.72.4" + metro-hermes-compiler "0.72.4" + metro-source-map "0.72.4" + metro-transform-plugins "0.72.4" nullthrows "^1.1.1" -metro@0.70.3, metro@^0.70.1: - version "0.70.3" - resolved "https://registry.yarnpkg.com/metro/-/metro-0.70.3.tgz#4290f538ab5446c7050e718b5c5823eea292c5c2" - integrity sha512-uEWS7xg8oTetQDABYNtsyeUjdLhH3KAvLFpaFFoJqUpOk2A3iygszdqmjobFl6W4zrvKDJS+XxdMR1roYvUhTw== +metro@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.72.4.tgz#fdfc43b3329388b5a3e8856727403f93a8c05250" + integrity sha512-UBqL2fswJjsq2LlfMPV4ArqzLzjyN0nReKRijP3DdSxZiaJDG4NC9sQoVJHbH1HP5qXQMAK/SftyAx1c1kuy+w== dependencies: "@babel/code-frame" "^7.0.0" "@babel/core" "^7.14.0" @@ -12055,27 +12384,28 @@ metro@0.70.3, metro@^0.70.1: error-stack-parser "^2.0.6" fs-extra "^1.0.0" graceful-fs "^4.2.4" - hermes-parser "0.6.0" + hermes-parser "0.8.0" image-size "^0.6.0" invariant "^2.2.4" - jest-haste-map "^27.3.1" jest-worker "^27.2.0" + jsc-safe-url "^0.2.2" lodash.throttle "^4.1.1" - metro-babel-transformer "0.70.3" - metro-cache "0.70.3" - metro-cache-key "0.70.3" - metro-config "0.70.3" - metro-core "0.70.3" - metro-hermes-compiler "0.70.3" - metro-inspector-proxy "0.70.3" - metro-minify-uglify "0.70.3" - metro-react-native-babel-preset "0.70.3" - metro-resolver "0.70.3" - metro-runtime "0.70.3" - metro-source-map "0.70.3" - metro-symbolicate "0.70.3" - metro-transform-plugins "0.70.3" - metro-transform-worker "0.70.3" + metro-babel-transformer "0.72.4" + metro-cache "0.72.4" + metro-cache-key "0.72.4" + metro-config "0.72.4" + metro-core "0.72.4" + metro-file-map "0.72.4" + metro-hermes-compiler "0.72.4" + metro-inspector-proxy "0.72.4" + metro-minify-uglify "0.72.4" + metro-react-native-babel-preset "0.72.4" + metro-resolver "0.72.4" + metro-runtime "0.72.4" + metro-source-map "0.72.4" + metro-symbolicate "0.72.4" + metro-transform-plugins "0.72.4" + metro-transform-worker "0.72.4" mime-types "^2.1.27" node-fetch "^2.2.0" nullthrows "^1.1.1" @@ -12480,6 +12810,11 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" +mixpanel-react-native@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/mixpanel-react-native/-/mixpanel-react-native-2.4.1.tgz#d63bf160875e9a0b7e3a50f395ad7aaa6d8c09a5" + integrity sha512-NNzXxOxX83FSIKN3x9vY49a13mk2syshWDcXvkfljiQ2bgwk0suA9PKYQNOkbwT/Af53oWt06JMe0xP4RUOyCA== + mkdirp2@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp2/-/mkdirp2-1.0.4.tgz#56de1f8f5c93cf2199906362eba0f9f262ee4437" @@ -12576,11 +12911,6 @@ nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nanoid@^3.1.15: - version "3.3.3" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" - integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== - nanoid@^3.1.20, nanoid@^3.1.23: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" @@ -12752,6 +13082,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-releases@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" @@ -12885,10 +13220,10 @@ ob1@0.58.0: resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.58.0.tgz#484a1e9a63a8b79d9ea6f3a83b2a42110faac973" integrity sha512-uZP44cbowAfHafP1k4skpWItk5iHCoRevMfrnUvYCfyNNPPJd3rfDCyj0exklWi2gDXvjlj2ObsfiqP/bs/J7Q== -ob1@0.70.3: - version "0.70.3" - resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.70.3.tgz#f48cd5a5abf54b0c423b1b06b6d4ff4d049816cb" - integrity sha512-Vy9GGhuXgDRY01QA6kdhToPd8AkLdLpX9GjH5kpqluVqTu70mgOm7tpGoJDZGaNbr9nJlJgnipqHJQRPORixIQ== +ob1@0.72.4: + version "0.72.4" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.72.4.tgz#d2ddedb09fb258d69490e8809157518a62b75506" + integrity sha512-/iPJKpXpVEZS0subUvjew4ept5LTBxj1hD20A4mAj9CJkGGPgvbBlfYtFEBubBkk4dv4Ef5lajsnRBYPxF74cQ== object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" @@ -13641,7 +13976,7 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -plist@^3.0.2, plist@^3.0.5: +plist@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.5.tgz#2cbeb52d10e3cdccccf0c11a63a85d830970a987" integrity sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA== @@ -13800,7 +14135,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -promise@^8.2.0: +promise@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== @@ -13932,16 +14267,6 @@ query-string@6.10.1: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -query-string@^6.13.6: - version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== - dependencies: - decode-uri-component "^0.2.0" - filter-obj "^1.1.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - query-string@^6.8.2: version "6.13.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad" @@ -13951,6 +14276,16 @@ query-string@^6.8.2: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" +query-string@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -14038,25 +14373,25 @@ react-deep-force-update@^1.0.0: resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.1.2.tgz#3d2ae45c2c9040cbb1772be52f8ea1ade6ca2ee1" integrity sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA== -react-devtools-core@4.24.0: - version "4.24.0" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.24.0.tgz#7daa196bdc64f3626b3f54f2ff2b96f7c4fdf017" - integrity sha512-Rw7FzYOOzcfyUPaAm9P3g0tFdGqGq2LLiAI+wjYcp6CsF3DeeMrRS3HZAho4s273C29G/DJhx0e8BpRE/QZNGg== +react-devtools-core@4.27.7: + version "4.27.7" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.7.tgz#458a6541483078d60a036c75bf88f54c478086ec" + integrity sha512-12N0HrhCPbD76Z7SkyJdGdXdPGouUsgV6tlEsbSpAnLDO06tjXZP+irht4wPdYwJAJRQ85DxL48eQoz7UmrSuQ== dependencies: shell-quote "^1.6.1" ws "^7" +react-freeze@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d" + integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== + react-is@^16.12.0, react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.12.0 || ^17.0.0": - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.1.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== @@ -14110,14 +14445,14 @@ react-native-calendar-events@2.2.0: resolved "https://registry.yarnpkg.com/react-native-calendar-events/-/react-native-calendar-events-2.2.0.tgz#6cb78bf712457ca9928dae95b428c91e7b895ae0" integrity sha512-tNUbhT6Ief0JM4OQzQAaz1ri0+MCcAoHptBcEXCz2g7q3A05pg62PR2Dio4F9t2fCAD7Y2+QggdY1ycAsF3Tsg== -react-native-codegen@^0.69.2: - version "0.69.2" - resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.69.2.tgz#e33ac3b1486de59ddae687b731ddbfcef8af0e4e" - integrity sha512-yPcgMHD4mqLbckqnWjFBaxomDnBREfRjDi2G/WxNyPBQLD+PXUEmZTkDx6QoOXN+Bl2SkpnNOSsLE2+/RUHoPw== +react-native-codegen@^0.70.7: + version "0.70.7" + resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.70.7.tgz#8f6b47a88740ae703209d57b7605538d86dacfa6" + integrity sha512-qXE8Jrhc9BmxDAnCmrHFDLJrzgjsE/mH57dtC4IO7K76AwagdXNCMRp5SA8XdHJzvvHWRaghpiFHEMl9TtOBcQ== dependencies: "@babel/parser" "^7.14.0" flow-parser "^0.121.0" - jscodeshift "^0.13.1" + jscodeshift "^0.14.0" nullthrows "^1.1.1" react-native-config@^1.4.5: @@ -14146,10 +14481,10 @@ react-native-device-info@^10.8.0: resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.8.0.tgz#c331c94d65542f86bac197f223ee064c7e10a9ec" integrity sha512-DE4/X82ZVhdcnR1Y21iTP46WSSJA/rHK3lmeqWfGGq1RKLwXTIdxmfbZZnYwryqJ+esrw2l4ND19qlgxDGby8A== -react-native-document-picker@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-9.0.1.tgz#a5ceec157f84dbadb85fe717c657569755f4c6ca" - integrity sha512-l2c2xChwsdjzZIV9QJc85buC3vXkM5ZuY4943yMDj3TiszJp1spmHNaRMZKYIh3yVwdD2jENm0DBU5AWa+jhLg== +react-native-document-picker@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-9.1.1.tgz#54390c500396b61ed697095ef8a6f9cbe0977bc9" + integrity sha512-BW+7DbsILuFThlBm7NUFVUmKKf6Awkcf9R0q8wiCU2DlGGtAKQTt2iHpO5+Dn/7WMPB+rqNv3X1HsmJQ0t5R3g== dependencies: invariant "^2.2.4" @@ -14161,6 +14496,11 @@ react-native-drawer@2.5.1: prop-types "^15.5.8" tween-functions "^1.0.1" +react-native-easing-gradient@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-native-easing-gradient/-/react-native-easing-gradient-1.1.1.tgz#8d099795d7a0065e67d37a8489bffe354ffb94fa" + integrity sha512-KrGU97kPTMrLNzVXxFirFMgl65uo70gVNoKw2OngDzcDfoswb648I3ggeCnoY+JgMWkPyx4LlG7n3ZNgSPFnLw== + react-native-easy-grid@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/react-native-easy-grid/-/react-native-easy-grid-0.2.2.tgz#f0be33620be1ebe2d2295918eb58b0a27e8272ab" @@ -14201,10 +14541,10 @@ react-native-fs@^2.18.0: base-64 "^0.1.0" utf8 "^3.0.0" -react-native-gesture-handler@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.12.0.tgz#59ca9d97e4c71f70b9c258f14a1a081f4c689976" - integrity sha512-rr+XwVzXAVpY8co25ukvyI38fKCxTQjz7WajeZktl8qUPdh1twnSExgpT47DqDi4n+m+OiJPAnHfZOkqqAQMOg== +react-native-gesture-handler@^2.12.0, react-native-gesture-handler@^2.12.1: + version "2.15.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.15.0.tgz#f8e6c0451a7bdf065edb7b9be605480db402baa0" + integrity sha512-cmMGW8k86o/xgVTBZZOPohvR5re4Vh65PUxH4HbBBJAYTog4aN4wTVTUlnoky01HuSN8/X4h3tI/K3XLPoDnsg== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" @@ -14219,10 +14559,10 @@ react-native-get-random-values@^1.7.0: dependencies: fast-base64-decode "^1.0.0" -react-native-gradle-plugin@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz#96602f909745239deab7b589443f14fce5da2056" - integrity sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g== +react-native-gradle-plugin@^0.70.3: + version "0.70.3" + resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8" + integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A== react-native-haptic-feedback@^2.0.2: version "2.0.2" @@ -14251,11 +14591,6 @@ react-native-iphone-x-helper@^1.0.3: resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.1.tgz#645e2ffbbb49e80844bb4cbbe34a126fda1e6772" integrity sha512-/VbpIEp8tSNNHIvstuA3Swx610whci1Zpc9mqNkqn14DkMbw+ORviln2u0XyHG1kPvvwTNGZY6QpeFwxYaSdbQ== -react-native-iphone-x-helper@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" - integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== - react-native-keyboard-aware-scroll-view@^0.9.5: version "0.9.5" resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz#e2e9665d320c188e6b1f22f151b94eb358bf9b71" @@ -14282,17 +14617,10 @@ react-native-masked-text@^1.13.0: date-and-time "0.9.0" tinymask "1.0.2" -react-native-mixpanel@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/react-native-mixpanel/-/react-native-mixpanel-1.2.0.tgz#17c24151cf2039482cd20decc54ccf0e49ccebc5" - integrity sha512-Xy+KEIEVFidWuASBdexZquXTJBb3JCyFM7xSgnIOZt5PRzpARGFrYMDErTUZ1wSPK+KFaf1oylInOoC2DddI/Q== - -react-native-modal-datetime-picker@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-10.2.0.tgz#3ec5c299e8bdc9fd2ec0a1a6642a459c408b910f" - integrity sha512-eMQb3EPqHx47WrlLPTrXvmLZDwGwGH//WTWiQBUvJ+6ehDeuTjIn8/v/ANv8wxCCrt4NeHem8FQY3Z5fa4fRgQ== - dependencies: - prop-types "^15.7.2" +react-native-pager-view@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775" + integrity sha512-dqVpXWFtPNfD3D2QQQr8BP+ullS5MhjRJuF8Z/qml4QTILcrWaW8F5iAxKkQR3Jl0ikcEryG/+SQlNcwlo0Ggg== react-native-pdf-thumbnail@^1.2.1: version "1.2.1" @@ -14378,10 +14706,13 @@ react-native-screen-brightness@^2.0.0-alpha: resolved "https://registry.yarnpkg.com/react-native-screen-brightness/-/react-native-screen-brightness-2.0.0-alpha.tgz#2fc30cf711cfafa5e0aec685f0fce51ce65e0052" integrity sha512-NdJPptcWiFwG9aypm0awYv9d/aoZSJetXPFkx6w/UuNX9SRr5YTKXMSND/2lwuSOw6uGU9mQ0f4dBuGZSNgzow== -react-native-screens@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.18.1.tgz#47b9991c6f762d00d0ed3233e5283d523e859885" - integrity sha512-r5WZLpmx2hHjC1RgMdPq5YpSU9tEhBpUaZ5M1SUtNIONyiLqQVxabhRCINdebIk4depJiIl7yw2Q85zJyeX6fw== +react-native-screens@^3.30.1: + version "3.30.1" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.30.1.tgz#254f5cbbeed649492da112756f85ecae3ccf313e" + integrity sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg== + dependencies: + react-freeze "^1.0.0" + warn-once "^0.1.0" react-native-sha256@1.2.3: version "1.2.3" @@ -14408,18 +14739,20 @@ react-native-svg-transformer@^0.14.3: path-dirname "^1.0.2" semver "^5.6.0" -react-native-svg@^12.3.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.3.0.tgz#40f657c5d1ee366df23f3ec8dae76fd276b86248" - integrity sha512-ESG1g1j7/WLD7X3XRFTQHVv0r6DpbHNNcdusngAODIxG88wpTWUZkhcM3A2HJTb+BbXTFDamHv7FwtRKWQ/ALg== +react-native-svg@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.1.0.tgz#07c75f29b1d641faba50144c7ccd21604b368420" + integrity sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A== dependencies: - css-select "^4.2.1" - css-tree "^1.0.0-alpha.39" + css-select "^5.1.0" + css-tree "^1.1.3" -react-native-tab-view@^2.x: - version "2.16.0" - resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz#cae72c7084394bd328fac5fefb86cd966df37a86" - integrity sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg== +react-native-tab-view@3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.5.2.tgz#2789b8af6148b16835869566bf13dc3b0e6c1b46" + integrity sha512-nE5WqjbeEPsWQx4mtz81QGVvgHRhujTNIIZiMCx3Bj6CBFDafbk7XZp9ocmtzXUQaZ4bhtVS43R4FIiR4LboJw== + dependencies: + use-latest-callback "^0.1.5" react-native-vector-icons@^7.0.0: version "7.0.0" @@ -14460,15 +14793,15 @@ react-native-xml2js@^1.0.3: timers "0.1.x" xmlbuilder "8.2.x" -react-native@0.69.9: - version "0.69.9" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.69.9.tgz#c988dfc2e21b3b586d35a8cc57b102537e760edc" - integrity sha512-I1xqIn47RWxBToO4E6yqyIPSaK9mZnMiscMfrFpWjQr3Gdkicr9y+twmtrRszxaLdQLjHzh/M3y4qOqc3hZnpg== +react-native@0.70.15: + version "0.70.15" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.70.15.tgz#65f2c5c399ff8e2a892cef9b094cc0888653a874" + integrity sha512-pm2ZPpA+m0Kl0THAy2fptnp7B9+QPexpfad9fSXfqjPufrXG2alwW8kYCn2EO5ZUX6bomZjFEswz6RzdRN/p9A== dependencies: "@jest/create-cache-key-function" "^27.0.1" - "@react-native-community/cli" "^8.0.4" - "@react-native-community/cli-platform-android" "^8.0.4" - "@react-native-community/cli-platform-ios" "^8.0.4" + "@react-native-community/cli" "9.3.5" + "@react-native-community/cli-platform-android" "9.3.4" + "@react-native-community/cli-platform-ios" "9.3.0" "@react-native/assets" "1.0.0" "@react-native/normalize-color" "2.0.0" "@react-native/polyfills" "2.0.0" @@ -14476,24 +14809,23 @@ react-native@0.69.9: anser "^1.4.9" base64-js "^1.1.2" event-target-shim "^5.0.1" - hermes-engine "~0.11.0" invariant "^2.2.4" jsc-android "^250230.2.1" memoize-one "^5.0.0" - metro-react-native-babel-transformer "0.70.3" - metro-runtime "0.70.3" - metro-source-map "0.70.3" + metro-react-native-babel-transformer "0.72.4" + metro-runtime "0.72.4" + metro-source-map "0.72.4" mkdirp "^0.5.1" nullthrows "^1.1.1" pretty-format "^26.5.2" - promise "^8.2.0" - react-devtools-core "4.24.0" - react-native-codegen "^0.69.2" - react-native-gradle-plugin "^0.0.7" + promise "^8.3.0" + react-devtools-core "4.27.7" + react-native-codegen "^0.70.7" + react-native-gradle-plugin "^0.70.3" react-refresh "^0.4.0" - react-shallow-renderer "16.15.0" + react-shallow-renderer "^16.15.0" regenerator-runtime "^0.13.2" - scheduler "^0.21.0" + scheduler "^0.22.0" stacktrace-parser "^0.1.3" use-sync-external-store "^1.0.0" whatwg-fetch "^3.0.0" @@ -14524,7 +14856,7 @@ react-refresh@^0.4.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53" integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA== -react-shallow-renderer@16.15.0: +react-shallow-renderer@^16.15.0: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== @@ -14532,22 +14864,14 @@ react-shallow-renderer@16.15.0: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" -react-shallow-renderer@^16.13.1: - version "16.14.1" - resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" - integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== +react-test-renderer@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.1.0.tgz#35b75754834cf9ab517b6813db94aee0a6b545c3" + integrity sha512-OfuueprJFW7h69GN+kr4Ywin7stcuqaYAt1g7airM5cUgP0BoF5G5CXsPGmXeDeEkncb2fqYNECO4y18sSqphg== dependencies: - object-assign "^4.1.1" - react-is "^16.12.0 || ^17.0.0" - -react-test-renderer@18.0.0: - version "18.0.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.0.0.tgz#fa403d625ea9478a70ace43db88833f6c3a5bb4c" - integrity sha512-SyZTP/FSkwfiKOZuTZiISzsrC8A80KNlQ8PyyoGoOq+VzMAab6Em1POK/CiX3+XyXG6oiJa1C53zYDbdrJu9fw== - dependencies: - react-is "^18.0.0" - react-shallow-renderer "^16.13.1" - scheduler "^0.21.0" + react-is "^18.1.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.22.0" react-transform-hmr@^1.0.4: version "1.0.4" @@ -14565,10 +14889,10 @@ react-tween-state@^0.1.5: raf "^3.1.0" tween-functions "^1.0.1" -react@18.0.0: - version "18.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" - integrity sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A== +react@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" + integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== dependencies: loose-envify "^1.1.0" @@ -14701,12 +15025,12 @@ readline@^1.3.0: resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" integrity sha1-xYDXfvLPyHUrEySYBg3JeTp6wBw= -recast@^0.20.4: - version "0.20.5" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.5.tgz#8e2c6c96827a1b339c634dd232957d230553ceae" - integrity sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ== +recast@^0.21.0: + version "0.21.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.21.5.tgz#e8cd22bb51bcd6130e54f87955d33a2b2e57b495" + integrity sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg== dependencies: - ast-types "0.14.2" + ast-types "0.15.2" esprima "~4.0.0" source-map "~0.6.1" tslib "^2.0.1" @@ -15384,10 +15708,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" - integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== +scheduler@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" + integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== dependencies: loose-envify "^1.1.0" @@ -15406,6 +15730,11 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" @@ -16961,6 +17290,14 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-browserslist-db@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" @@ -16999,6 +17336,11 @@ use-isomorphic-layout-effect@^1.0.0: resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== +use-latest-callback@^0.1.5, use-latest-callback@^0.1.7: + version "0.1.9" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" + integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -17153,6 +17495,11 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warn-once@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" + integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"