diff --git a/.eslintrc.json b/.eslintrc.json index b9740a3a11..9f17a27704 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,7 @@ { + "env": { + "es6": true + }, "root": true, "ignorePatterns": [ "projects/**/*", @@ -81,7 +84,11 @@ "@typescript-eslint/no-empty-function": ["error"], "@typescript-eslint/no-empty-interface": ["error"], "@typescript-eslint/no-inferrable-types": ["error"], - "no-unused-expressions": ["error"] + "no-unused-expressions": ["error"], + "no-console": ["error", { "allow": [""] }], + "custom-rules/one-interface-per-file": "error", + "custom-rules/one-enum-per-file": "error", + "custom-rules/prefer-semantic-extension-name": "error" } }, { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 81e316a47f..aaf7f5ddec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # This file specifies codeowners for different parts of the Fyle Mobile App. -* @Chethan-Fyle \ No newline at end of file +* @Chethan-Fyle @arjunaj5 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 957e3cc6de..b583464919 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,3 @@ -### Description -copilot:summary - -copilot:poem - -### Walkthrough -copilot:walkthrough - ## Clickup Please add link here diff --git a/.github/workflows/appflow-master.yml b/.github/workflows/appflow-master.yml index e8d7ac8937..3bad3ab4e5 100644 --- a/.github/workflows/appflow-master.yml +++ b/.github/workflows/appflow-master.yml @@ -19,7 +19,7 @@ jobs: native-config: staging upload-artifact: Android - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: download with: name: 'Android' @@ -42,12 +42,13 @@ jobs: app-id: 32316914 platform: iOS build-type: development + build-stack: macOS - 2024.04 - Apple silicon certificate: Fyle signing environment: staging native-config: prod upload-artifact: ios - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: ios with: name: 'ios' diff --git a/.github/workflows/appflow-release-branch.yml b/.github/workflows/appflow-release-branch.yml index e35e4a3e3d..75f9b829d5 100644 --- a/.github/workflows/appflow-release-branch.yml +++ b/.github/workflows/appflow-release-branch.yml @@ -20,7 +20,7 @@ jobs: native-config: staging upload-artifact: Android - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: download with: name: 'Android' @@ -43,12 +43,13 @@ jobs: app-id: 32316914 platform: iOS build-type: development + build-stack: macOS - 2024.04 - Apple silicon certificate: Fyle signing environment: staging native-config: prod upload-artifact: ios - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: ios with: name: 'ios' diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml index 4ffa295b34..bc7397167b 100644 --- a/.github/workflows/create-release-branch.yml +++ b/.github/workflows/create-release-branch.yml @@ -1,8 +1,8 @@ name: Create Release Branch on: schedule: - # Run every Friday at 11:30 AM UTC (5:00 PM IST) - - cron: '30 11 * * FRI' + # Run every Monday at 11:30 AM UTC (5:00 PM IST) + - cron: '30 11 * * MON' jobs: create-release-branch: runs-on: ubuntu-latest @@ -14,7 +14,7 @@ jobs: - name: Get Release Branch Name run: | echo "release_branch=mobile_release_$(date +%Y_%m_%d)" >> $GITHUB_ENV - echo "last_release_branch=mobile_release_$(date -d 'last friday' +%Y_%m_%d)" >> $GITHUB_ENV + echo "last_release_branch=mobile_release_$(date -d 'last monday' +%Y_%m_%d)" >> $GITHUB_ENV - name: Check if release branch exists run: | if git ls-remote --exit-code --heads origin ${{env.release_branch}}; then diff --git a/.github/workflows/internal-release.yml b/.github/workflows/internal-release.yml index 1dfc5c1025..c7a3f744f0 100644 --- a/.github/workflows/internal-release.yml +++ b/.github/workflows/internal-release.yml @@ -27,6 +27,7 @@ jobs: app-id: 32316914 platform: iOS build-type: App Store + build-stack: macOS - 2024.04 - Apple silicon certificate: iOS Prod Release environment: production native-config: prod diff --git a/.github/workflows/manual-appflow-1.yml b/.github/workflows/manual-appflow-1.yml index 3c5f45c23f..bd248caed4 100644 --- a/.github/workflows/manual-appflow-1.yml +++ b/.github/workflows/manual-appflow-1.yml @@ -22,7 +22,7 @@ jobs: native-config: staging upload-artifact: Android - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: download with: name: 'Android' @@ -45,12 +45,13 @@ jobs: app-id: 32316914 platform: iOS build-type: development + build-stack: macOS - 2024.04 - Apple silicon certificate: Fyle signing environment: staging native-config: prod upload-artifact: ios - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: ios with: name: 'ios' diff --git a/.github/workflows/manual-appflow-with-inputs.yml b/.github/workflows/manual-appflow-with-inputs.yml index 4d1cae7dc5..65744248e1 100644 --- a/.github/workflows/manual-appflow-with-inputs.yml +++ b/.github/workflows/manual-appflow-with-inputs.yml @@ -14,6 +14,11 @@ on: options: - staging - production + - blackwidow + - thanos + - thor + - drstrange + - hulk jobs: build-android: @@ -33,7 +38,7 @@ jobs: native-config: prod upload-artifact: Android - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: download with: name: 'Android' @@ -61,12 +66,13 @@ jobs: app-id: 32316914 platform: iOS build-type: development + build-stack: macOS - 2024.04 - Apple silicon certificate: Fyle signing environment: ${{ github.event.inputs.name }} native-config: prod upload-artifact: ios - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 id: ios with: name: 'ios' diff --git a/.github/workflows/pr-checks-config.yml b/.github/workflows/pr-checks-config.yml index 616f523ba6..baa05ebf14 100644 --- a/.github/workflows/pr-checks-config.yml +++ b/.github/workflows/pr-checks-config.yml @@ -1,10 +1,10 @@ pr_checks: title: - name: 'prefix_check' - regex: '^(fix|feat|test):' - message_if_not_matching: 'PR title must start with "fix:" or "feat:" or "test:"' + regex: '^(?i)(fix|feat|test|chore|refactor|build):' + message_if_not_matching: 'PR title must start with "fix:", "feat:", "chore:", "refactor:", or "test:" (case-insensitive)' description: - name: 'clickup_check' - regex: 'app.clickup.com' - message_if_not_matching: 'PR description must contain a link to a clickup' + regex: '(?i)app.clickup.com' + message_if_not_matching: 'PR description must contain a link to a ClickUp (case-insensitive)' diff --git a/.github/workflows/pr-size-label.yml b/.github/workflows/pr-size-label.yml index 27230432dc..f6cbb36d5b 100644 --- a/.github/workflows/pr-size-label.yml +++ b/.github/workflows/pr-size-label.yml @@ -1,13 +1,13 @@ -name: PR Checks +name: Pull Request Labeling on: [pull_request] jobs: size: runs-on: ubuntu-latest - name: Label the size of PR + name: Label the PR size steps: - - uses: "pascalgn/size-label-action@v0.4.3" + - uses: "pascalgn/size-label-action@v0.5.4" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -18,4 +18,4 @@ jobs: "50": "M", "250": "L", "800": "XL" - } \ No newline at end of file + } diff --git a/.gitignore b/.gitignore index 9d7c944501..aabf4d2f32 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ package-lock.json setup.sh .env .angular + +# Sentry Config File +.sentryclirc + diff --git a/README.md b/README.md index 5f4ce72908..591dfd3ac0 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,274 @@ -# fyle-mobile-app +# Fyle Mobile App -# Node version +This repository holds the codebase for the Fyle Mobile App. This document provides the tools and guidelines to set up, develop, test, and deploy the app efficiently. Built using the [Ionic Framework](https://ionicframework.com/), it supports both Android and iOS platforms. -Please install node v14.17.2 or above via nvm. +## ๐Ÿ“‘ Table of Contents -## How to run this locally? +- [๐Ÿ”ง Prerequisites](#-prerequisites) +- [โœจ Quick Setup](#-quick-setup) +- [๐Ÿ‘€ Environment Setup](#-environment-setup) +- [โ“ Troubleshooting](#-troubleshooting) +- [๐Ÿ“‚ Project Structure](#-project-structure) +- [๐Ÿงช Testing](#-testing) + - [Viewing Coverage Reports](#viewing-coverage-reports) +- [๐Ÿ“ฑ Running on Devices](#-running-on-devices) + - [Android](#android) + - [iOS](#ios) +- [๐Ÿ“Š Deployment](#-deployment) + - [Running Appflow workflow manually on a private branch](#running-appflow-workflow-manually-on-a-private-branch) +- [๐Ÿ™ Further Help](#-further-help) + +
+ +## ๐Ÿ”ง Prerequisites + +- Node.js: Install Node.js (v14.17.2 or higher) using [nvm](https://github.com/nvm-sh/nvm). + +- Ionic CLI: Follow the instructions [here](https://ionicframework.com/docs/cli) and Install Ionic globally with `npm` by running: + +```bash + npm install -g @ionic/cli +``` + +
+ +## โœจ Quick Setup + +Follow the following steps to run the app locally in your browser: + +1. **Clone the repository:** + +```bash +git clone https://github.com/fylein/fyle-mobile-app.git +cd fyle-mobile-app +``` + +2. **Install dependencies:** + +```bash +npm install +``` + +3. **Set Environment Variables**: Add the environment files corresponding to the build you want in the environment folder. ([Follow Environment Setup](#-environment-setup)) -1. **Install Ionic CLI**: If you haven't already, follow the instructions [here](https://ionicframework.com/docs/cli) to install the Ionic CLI. -2. **Install Dependencies**: Run `npm install` in your terminal to install all the necessary dependencies. -3. **Set Environment Variables**: Add the environment files corresponding to the build you want in the environment folder. 4. **Run Locally**: Use the following command to run the application locally: + +```bash +ionic serve -c [env_name] +``` + +> [!NOTE] +> Replace env_name with the name of the environment file you want to use. For example, if you > have an environment file named staging, you would run: +> +> ```bash +> ionic serve -c staging +> ``` + +
+ +## ๐Ÿ‘€ Environment Setup + +For setting environment variables + +- **Environment Files:** Ping the mobile app team and get the necessary environment files (environment.[env_name].ts). +- Place them inside the `/src/environments` folder + +> [!IMPORTANT] +> Do not make any changes to the `environment.ts` file - this is a template folder for creating > configurations. Also, make sure not to put staging environments in this file. + +
+ +## โ“ Troubleshooting + +Here are some common issues and how to fix them: + +1. If you encounter any similar error like this: + `Property 'LIVE_UPDATE_APP_VERSION' does not exist on type ...` + +```bash + Property 'LIVE_UPDATE_APP_VERSION' does not exist on type + '{ production: boolean; NAME: string; CLUSTER_DOMAIN: string; ROOT_URL: string; ROUTER_API_ENDPOINT: string; + ANDROID_CLIENT_ID: string; IP_FIND_KEY: string; GOOGLE_MAPS_API_KEY: string; FRESHCHAT_TOKEN: string; + SENTRY_DSN: string; REFINER_NPS_FORM_ID: string; }' +``` + +**Solution**: + +Ensure that you have the latest environment.staging.ts file. This file might have been updated with new properties that are missing in your current version. + +2. `Error: Cannot GET /` when running the app locally in the browser + +This error typically occurs when some of the dependencies listed in `package.json` are not installed properly. + +**Solution**: + +- Delete the node_modules folder by running: + +```bash +rm -rf node_modules +``` + +- Reinstall the dependencies by running: + ```bash -ionic serve -c env_name +npm install ``` -Replace env_name with the name of the environment file you want to use. For example, if you have an environment file named staging, you would run: + +This will ensure that all required packages are correctly installed, resolving the issue. + +3. Unable to create PR + +**Solution**: + +- Ping the mobile app team for write access to the repository + +
+ +## ๐Ÿ“‚ Project Structure + +```bash +. +โ”œโ”€โ”€ .angular/ # Angular-related configuration +โ”œโ”€โ”€ .github/ # GitHub-specific configuration (e.g., workflows) +โ”œโ”€โ”€ .husky/ # Git hooks for pre-commit, etc. +โ”œโ”€โ”€ android/ # Android-specific configuration and source files +โ”œโ”€โ”€ coverage/ # Code coverage reports +โ”œโ”€โ”€ e2e/ # End-to-end tests +โ”œโ”€โ”€ eslint-custom-rules/ # Custom ESLint rules +โ”œโ”€โ”€ hooks/ # Custom hooks for the project +โ”œโ”€โ”€ ios/ # iOS-specific configuration and source files +โ”œโ”€โ”€ node_modules/ # Installed Node.js dependencies +โ”œโ”€โ”€ resources/ # Shared resources like images, fonts, etc. +โ”œโ”€โ”€ src/ # Main source code of the app +โ”‚ โ”œโ”€โ”€ app/ # Application core +โ”‚ โ”‚ โ”œโ”€โ”€ auth/ # Authentication module +โ”‚ โ”‚ โ”œโ”€โ”€ core/ # Core application utilities and services +โ”‚ โ”‚ โ”œโ”€โ”€ deep-link-redirection/ # Deep link handling module +โ”‚ โ”‚ โ”œโ”€โ”€ fyle/ # Fyle-specific features or modules +โ”‚ โ”‚ โ”œโ”€โ”€ post-verification/ # Post-verification module +โ”‚ โ”‚ โ”œโ”€โ”€ shared/ # Shared components, directives, services, or icons +โ”‚ โ”‚ โ”œโ”€โ”€ app-routing.module.ts # Routing configuration for the app +โ”‚ โ”‚ โ”œโ”€โ”€ app.component.html # Main HTML template for the app +โ”‚ โ”‚ โ”œโ”€โ”€ app.component.scss # Main SCSS styles for the app +โ”‚ โ”‚ โ”œโ”€โ”€ app.component.spec.ts # Unit tests for the main app component +โ”‚ โ”‚ โ”œโ”€โ”€ app.component.ts # Main app component logic +โ”‚ โ”‚ โ”œโ”€โ”€ app.module.ts # Root module of the app +โ”‚ โ”‚ โ”œโ”€โ”€ constants.ts # Application-wide constants +โ”‚ โ”œโ”€โ”€ assets/ # Static assets like images, icons, font, etc. +โ”‚ โ”œโ”€โ”€ environments/ # Environment file-specific configuration +โ”‚ โ”œโ”€โ”€ theme/ # Application themes (scss) +โ”‚ โ”œโ”€โ”€ global.scss # Global styles for the app +โ”‚ โ”œโ”€โ”€ index.html # Main HTML file of the application +โ”‚ โ”œโ”€โ”€ main.ts # Main entry point for Angular app +โ”‚ โ”œโ”€โ”€ polyfills.ts # Polyfills needed by Angular to load before the app +โ”‚ โ”œโ”€โ”€ test.ts # File is required by karma.conf.js and loads recursively all the .spec +โ”‚ โ”œโ”€โ”€ zone-flags.ts # Zone.js configuration flags +โ”œโ”€โ”€ .eslintrc.json # configuration file for ESLin +โ”œโ”€โ”€ .gitignore # Files and folders ignored by Git +โ”œโ”€โ”€ .npmrc # NPM configuration +โ”œโ”€โ”€ .prettierignore # Files ignored by Prettier +โ”œโ”€โ”€ angular.json # Angular workspace configuration +โ”œโ”€โ”€ appflow.config.json # Configuration for Appflow +โ”œโ”€โ”€ browserslist # Browser compatibility configuration +โ”œโ”€โ”€ build_appflow.sh # Shell script for Appflow build +โ”œโ”€โ”€ build_prod.sh # Shell script for production build +โ”œโ”€โ”€ build_staging.sh # Shell script for staging build +โ”œโ”€โ”€ capacitor.config.ts # Capacitor configuration +โ”œโ”€โ”€ ionic.config.json # Ionic CLI configuration +โ”œโ”€โ”€ karma.conf.js # Karma test runner configuration +โ”œโ”€โ”€ LICENSE # License file +โ”œโ”€โ”€ package-lock.json # Lockfile for NPM dependencies +โ”œโ”€โ”€ package.json # Project dependencies and scripts +โ”œโ”€โ”€ README.md # Documentation for the project +โ”œโ”€โ”€ tsconfig.app.json # TypeScript configuration for the app +โ”œโ”€โ”€ tsconfig.json # Base TypeScript configuration +โ”œโ”€โ”€ tsconfig.spec.json # TypeScript configuration for tests +โ””โ”€โ”€ ... +``` + +
+ +## ๐Ÿงช Testing + +- Run unit tests: + +```bash +npm run test +``` + +- For non-parallel execution (Recommended method for preventing excessive CPU utilization and memory hogging.): + +```bash +npm run test:no-parallel +``` + +### Viewing Coverage Reports + +After running the tests, you can view the test coverage report by following these steps: + +- Open the generated `index.html` file present in the `app/coverage/index.html`. +- Metrics Explanation: +In this file, you will see 4 metrics for the files you have changed: + +* **Statements**: Percentage of executed statements +* **Branches**: Percentage of executed branches (e.g., conditions in if, else, switch statements, `&&`, `||`, `?` operators) +* **Functions**: Percentage of executed functions +* **Lines**: Percentage of executed lines of code +- To increase code coverage, write additional test cases to cover the missing metrics. + +
+ +## ๐Ÿ“ฑ Running on Devices + +### Android + +For running the app directly on an Android device for staging + +1. Build and sync the app: + ```bash -ionic serve -c staging +ionic capacitor run android -l --external --configuration=staging + ``` -# IMPORTANT -## For setting environment variables - - - Ping mobile app team for environment files - - Add them inside the environments folder - - Note: Do not make any changes to the environment.ts file - this is a template folder for creating configurations. Also, make sure not to put staging envs in this file. - - If you are getting errors like this: - ``` - Property 'LIVE_UPDATE_APP_VERSION' does not exist on type - '{ production: boolean; NAME: string; CLUSTER_DOMAIN: string; ROOT_URL: string; ROUTER_API_ENDPOINT: string; - ANDROID_CLIENT_ID: string; IP_FIND_KEY: string; GOOGLE_MAPS_API_KEY: string; FRESHCHAT_TOKEN: string; - SENTRY_DSN: string; REFINER_NPS_FORM_ID: string; }' - ``` - make sure you have the latest `environment.staging.ts` file. - -## For creating pull requests - - - Ping mobile app team for write access to the repository - -## Running unit tests - - - Run `ng test` - - Run `npm run test:no-parallel` to run tests without sharding (without parallel browsers). This is useful to avoid parallel execution and to prevent excessive CPU utilization and memory hogging. - -## Viewing coverage report - After running the tests, you can view the test coverage report by following these steps: - - - Open generated `index.html` file present in the `app/coverage/index.html`. - - Metrics Explanation: - In this file you would see 4 metrics for the files you have changed - - Statements: Percentage of executed statements. - - Branches: Percentage of executed branches (e.g., conditions in if, else, switch statements, `&&`, `||`, `?` operators used). - - Functions: Percentage of executed functions. - - Lines: Percentage of executed lines of code. - - To increase code coverage write additional test cases to cover the missing Metrics. - -## For running app directly in android device for staging - - - ionic capacitor run android -l --external --configuration=staging - It will open android studio, let it build index file and Gradle build for sometime - Then check that studio recognized the right device in the top bar. Press the run button. After every change, you make in the `src` directory. It will automatically build the app on the device. - -## For running app directly in ios device for staging - - - Add .env file to project (ping mobile app team for the file) - - Install Xcode from App store - - run ionic build --staging - - npx cap sync - - npx cap open ios - - In Xcode, select the connected device from the top bar and click on run button. - -## Push Notifications - **iOS** - - Add this line `ios/App/Podfile` file (open via code editor, not xcode) - - (After this line -> # Add your Pods here) - - pod 'FirebaseCore' - - pod 'Firebase/Messaging' - - Run this from terminal - - npx cap update ios - - Add these lines `ios/App/App/AppDelegate.swift` file - - (After this line -> import Capacitor) - - import FirebaseCore - - import FirebaseInstanceID - - import FirebaseMessaging - - FirebaseApp.configure() (After this line -> // Override point for customization after application launch.) - - Update the application_ function after #if USE_PUSH as described below - ```bash - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - Messaging.messaging().apnsToken = deviceToken - InstanceID.instanceID().instanceID { (result, error) in - if let error = error { - NotificationCenter.default.post(name: Notification.Name(CAPNotifications.DidFailToRegisterForRemoteNotificationsWithError.name()), object: error) - } else if let result = result { - NotificationCenter.default.post(name: Notification.Name(CAPNotifications.DidRegisterForRemoteNotificationsWithDeviceToken.name()), object: result.token) - } - } - } - ``` - - Now open Xcode and enable Push notifications from sigining and capabilites if it is not enabled. - - Follow these two articles if you face any issue - - https://devdactic.com/push-notifications-ionic-capacitor/ - - https://capacitorjs.com/docs/guides/push-notifications-firebase - -## Running Appflow workflow manually on a private branch - - - Click on [Actions](https://github.com/fylein/fyle-mobile-app2/actions) Tab - - From Workflows List, Select `Manual Workflow - Appflow` - - On the right hand side you can see the list of workflow run. - - In the list view you can see a `Run Workflow` button. Click on that button - - Select the branch on which you want to run the workflow from the dropdown available for `Use workflow from` - - Click on `Run Workflow` - - This will now run the workflow on your private branch and the diawi apk link and ipa links will be shared on slack - - ## Troubleshooting - Some common issues and how to fix them - - ### `Error: Cannot GET /` on running the app locally in browser - This happens because some packages listed in `package.json` are not installed locally. Delete `node_modules` and run `npm i` to fix this issue. +2. It will open Android Studio, and let it build index files and Gradle build for some time. Then check that the studio recognized the right device in the top bar. Press the run button. After every change, you make in the `src` directory. It will automatically build the app on the device. + +### iOS + +For running the app directly on an ios device for staging + +- Add .env file to project (ping mobile app team for the file) +- Install Xcode from the App Store +- Build and sync: + +```bash +ionic build --staging +npx cap sync +npx cap open ios +``` + +- Open Xcode, select the connected device from the top bar and click on the run button. + +
+ +## ๐Ÿ“Š Deployment + +### Running Appflow workflow manually on a private branch + +- Go to the [GitHub Actions tab](https://github.com/fylein/fyle-mobile-app2/actions). +- From Workflows List, Select `Manual Workflow - Appflow` +- On the right-hand side, you can see the list of workflow runs. +- In the list view you can see a `Run Workflow` button. Click on that button +- Select the branch on which you want to run the workflow from the dropdown available for `Use workflow from` +- Click on `Run Workflow` +- This will now run the workflow on your private branch, and the Diawi APK and IPA links will be shared on Slack + +
+ +## ๐Ÿ™ Further Help + +For access to environment files or repository write permissions, contact the mobile app team. + +For additional documentation, refer to the [Ionic Framework Docs](https://ionicframework.com/docs). + +
diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml index 797acea53e..16660f1d80 100644 --- a/android/.idea/runConfigurations.xml +++ b/android/.idea/runConfigurations.xml @@ -3,7 +3,14 @@ diff --git a/android/app/src/main/assets/splash-screen.png b/android/app/src/main/assets/splash-screen.png new file mode 100644 index 0000000000..f0b042ee04 Binary files /dev/null and b/android/app/src/main/assets/splash-screen.png differ diff --git a/android/app/src/main/ic_launcher_new_splash-playstore.png b/android/app/src/main/ic_launcher_new_splash-playstore.png new file mode 100644 index 0000000000..6155261e93 Binary files /dev/null and b/android/app/src/main/ic_launcher_new_splash-playstore.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_new_splash_background.xml b/android/app/src/main/res/drawable/ic_launcher_new_splash_background.xml new file mode 100644 index 0000000000..ca3826a46c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_new_splash_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml index ece36e455d..187227c55a 100644 --- a/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml @@ -1,34 +1,36 @@ - - - - - - - - + android:viewportWidth="768" + android:viewportHeight="768"> + + + + + + + + diff --git a/android/app/src/main/res/fyle_logo_square.png b/android/app/src/main/res/fyle_logo_square.png new file mode 100644 index 0000000000..119fe28ec1 Binary files /dev/null and b/android/app/src/main/res/fyle_logo_square.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash.xml index 3f18e9ffc9..56c2c7af0c 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash_round.xml index 3f18e9ffc9..56c2c7af0c 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash_round.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_new_splash_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png deleted file mode 100644 index 69076eb537..0000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..67e8d871a1 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..8c423e15c2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png index ccc4a4c960..96f00618e0 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png deleted file mode 100644 index fbb5189b69..0000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..227159afd1 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..e7d3141d81 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png deleted file mode 100644 index ed8fc23e71..0000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..023a246326 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..d708ec82d8 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png deleted file mode 100644 index 89e612ccf5..0000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..d6b1efd409 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..d5da2a651d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png deleted file mode 100644 index ddf7928b71..0000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..01b1fbc69b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..6e4518593c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/values/ic_launcher_new_splash_background.xml b/android/app/src/main/res/values/ic_launcher_new_splash_background.xml index 4294a955b9..cb5eb46f62 100644 --- a/android/app/src/main/res/values/ic_launcher_new_splash_background.xml +++ b/android/app/src/main/res/values/ic_launcher_new_splash_background.xml @@ -1,4 +1,4 @@ - #220033 + #FFFFFF \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index d0016f643b..a74326a208 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -18,6 +18,6 @@ \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle index 0fc8f45b1f..762792c935 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,7 +1,7 @@ ext { minSdkVersion = 24 compileSdkVersion = 33 - targetSdkVersion = 33 + targetSdkVersion = 34 androidxAppCompatVersion = '1.4.2' androidxCoreVersion = '1.8.0' androidxMaterialVersion = '1.6.1' diff --git a/build_appflow.sh b/build_appflow.sh index f2f52c3210..13f02eae99 100644 --- a/build_appflow.sh +++ b/build_appflow.sh @@ -1,3 +1,3 @@ -VERSION=`sentry-cli releases propose-version` ionic capacitor sync --prod --source-map -sentry-cli releases --org fyle-technologies-private-limi -p "$SENTRY_PROJECT_NAME" files "$VERSION" upload-sourcemaps www \ No newline at end of file +npm run sentry:sourcemaps +sentry-cli releases --org fyle-technologies-private-limi -p "$SENTRY_PROJECT_NAME" files "$LIVE_UPDATE_APP_VERSION" upload-sourcemaps www \ No newline at end of file diff --git a/capacitor.config.json b/capacitor.config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/capacitor.config.json @@ -0,0 +1 @@ +{} diff --git a/eslint-custom-rules/index.js b/eslint-custom-rules/index.js index 58d489f3c2..38ffdc886c 100644 --- a/eslint-custom-rules/index.js +++ b/eslint-custom-rules/index.js @@ -3,6 +3,9 @@ module.exports = { 'prefer-deep-freeze': require('./rules/eslint-plugin-prefer-deep-freeze'), 'space-before-it-blocks': require('./rules/eslint-plugin-space-before-it-blocks'), 'prefer-jasmine-matchers': require('./rules/eslint-plugin-prefer-jasmine-matchers'), - 'prefer-resolve-to-reject-with': require('./rules/eslint-plugin-prefer-resolve-to-reject-with') + 'prefer-resolve-to-reject-with': require('./rules/eslint-plugin-prefer-resolve-to-reject-with'), + 'one-interface-per-file': require('./rules/eslint-plugin-one-interface-per-file'), + 'one-enum-per-file': require('./rules/eslint-plugin-one-enum-per-file'), + 'prefer-semantic-extension-name': require('./rules/eslint-plugin-prefer-semantic-extension-name'), }, }; \ No newline at end of file diff --git a/eslint-custom-rules/rules/eslint-plugin-one-enum-per-file.js b/eslint-custom-rules/rules/eslint-plugin-one-enum-per-file.js new file mode 100644 index 0000000000..544feb183e --- /dev/null +++ b/eslint-custom-rules/rules/eslint-plugin-one-enum-per-file.js @@ -0,0 +1,31 @@ +module.exports = { + meta: { + type: "suggestion", + docs: { + description: "Enforce only one TypeScript enum per file.", + category: "TypeScript", + recommended: true, + }, + schema: [], + }, + + create: function(context) { + let enumCount = 0; + + return { + TSEnumDeclaration: function(node) { + enumCount++; + + if (enumCount > 1) { + context.report({ + node, + message: "Only one TypeScript enum is allowed per file.", + }); + } + }, + "Program:exit": function() { + enumCount = 0; // Reset the counters for the next file + }, + }; + }, +}; diff --git a/eslint-custom-rules/rules/eslint-plugin-one-interface-per-file.js b/eslint-custom-rules/rules/eslint-plugin-one-interface-per-file.js new file mode 100644 index 0000000000..755cab4963 --- /dev/null +++ b/eslint-custom-rules/rules/eslint-plugin-one-interface-per-file.js @@ -0,0 +1,45 @@ +module.exports = { + meta: { + type: "suggestion", + docs: { + description: "Enforce only one TypeScript interface or type per file.", + category: "TypeScript", + recommended: true, + }, + schema: [], + }, + + create: function(context) { + let interfaceCount = 0; + let typeCount = 0; + + return { + TSInterfaceDeclaration: function(node) { + interfaceCount++; + + if (interfaceCount > 1 || typeCount > 0) { + context.report({ + node, + message: "Only one TypeScript interface or type is allowed per file.", + }); + } + }, + + TSTypeAliasDeclaration: function(node) { + typeCount++; + + if (typeCount > 1 || interfaceCount > 0) { + context.report({ + node, + message: "Only one TypeScript interface or type is allowed per file.", + }); + } + }, + + "Program:exit": function() { + interfaceCount = 0; // Reset the counters for the next file + typeCount = 0; + }, + }; + }, +}; \ No newline at end of file diff --git a/eslint-custom-rules/rules/eslint-plugin-prefer-semantic-extension-name.js b/eslint-custom-rules/rules/eslint-plugin-prefer-semantic-extension-name.js new file mode 100644 index 0000000000..63cac7bc6f --- /dev/null +++ b/eslint-custom-rules/rules/eslint-plugin-prefer-semantic-extension-name.js @@ -0,0 +1,42 @@ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Enforce enums to be defined only in .enum.ts files', + category: 'Best Practices', + recommended: true + }, + schema: [], + }, + create: function(context) { + return { + TSEnumDeclaration: function(node) { + const fileName = context.getFilename(); + if (!fileName.endsWith('.enum.ts')) { + context.report({ + node, + message: 'Enums should be defined only in files with .enum.ts extension', + }); + } + }, + TSInterfaceDeclaration: function(node) { + const fileName = context.getFilename(); + if (!fileName.endsWith('.model.ts')) { + context.report({ + node, + message: 'Interfaces should be defined only in files with .model.ts extension', + }); + } + }, + TSTypeAliasDeclaration: function(node) { + const fileName = context.getFilename(); + if (!fileName.endsWith('.model.ts')) { + context.report({ + node, + message: 'Types should be defined only in files with .model.ts extension', + }); + } + } + }; + }, +}; diff --git a/fyle_logo.png b/fyle_logo.png new file mode 100644 index 0000000000..375696808a Binary files /dev/null and b/fyle_logo.png differ diff --git a/hooks/prebuild.js b/hooks/prebuild.js index 64f8141423..25e6c69e46 100644 --- a/hooks/prebuild.js +++ b/hooks/prebuild.js @@ -46,15 +46,6 @@ module.exports = function (ctx) { '/node_modules/@capacitor-community/camera-preview/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java', }; - // Adding GIT_COMMIT_SHA for sentry - var mainPath = path.resolve(process.cwd(), 'src/main.ts'); - var mainPathContent = fs.readFileSync(mainPath).toString(); - fs.writeFileSync( - mainPath, - mainPathContent.replace(/please-replace-your-git-commit-version/g, process.env.CI_GIT_COMMIT_SHA), - 'utf8' - ); - // Commenting Manifest.permission.RECORD_AUDIO on CameraPreview.java var cameraPreviewPath = path.resolve(process.cwd(), FILE_PATHS['android.cameraPreview']); var cameraPreviewContents = fs.readFileSync(cameraPreviewPath).toString(); diff --git a/hooks/utils/prod-environment.js b/hooks/utils/prod-environment.js index 7df06ffa18..ffdf3fc42d 100644 --- a/hooks/utils/prod-environment.js +++ b/hooks/utils/prod-environment.js @@ -12,9 +12,14 @@ export const environment = { GOOGLE_MAPS_API_KEY: '${process.env.FYLE_MOBILE_GOOGLE_MAPS_API_KEY}', FRESHCHAT_TOKEN: '${process.env.FYLE_MOBILE_FRESHCHAT_TOKEN}', SENTRY_DSN: '${process.env.FYLE_MOBILE_SENTRY_DSN}', + REFINER_NPS_FORM_PROJECT: '${process.env.REFINER_NPS_FORM_PROJECT}', REFINER_NPS_FORM_ID: '${process.env.REFINER_NPS_FORM_ID}', LAUNCH_DARKLY_CLIENT_ID: '${process.env.LAUNCH_DARKLY_CLIENT_ID}', LIVE_UPDATE_APP_VERSION: '${process.env.LIVE_UPDATE_APP_VERSION}', - SMARTLOOK_API_KEY: '${process.env.SMARTLOOK_API_KEY}' + SMARTLOOK_API_KEY: '${process.env.SMARTLOOK_API_KEY}', + MIXPANEL_PROJECT_TOKEN: '${process.env.MIXPANEL_PROJECT_TOKEN}', + USE_MIXPANEL_PROXY: '${process.env.USE_MIXPANEL_PROXY}', + ENABLE_MIXPANEL: '${process.env.ENABLE_MIXPANEL}', + YODLEE_FAST_LINK_URL: '${process.env.YODLEE_FAST_LINK_URL}' }; -` \ No newline at end of file +`; diff --git a/ios/App/App/Assets.xcassets/fyle_logo.png b/ios/App/App/Assets.xcassets/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/App/Assets.xcassets/fyle_logo.png and b/ios/App/App/Assets.xcassets/fyle_logo.png differ diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard index b2b38dd969..56d6415b9d 100644 --- a/ios/App/App/Base.lproj/LaunchScreen.storyboard +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -22,7 +22,7 @@ - + diff --git a/ios/App/App/fyle_logo.png b/ios/App/App/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/App/fyle_logo.png and b/ios/App/App/fyle_logo.png differ diff --git a/ios/App/fyle_logo.png b/ios/App/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/fyle_logo.png and b/ios/App/fyle_logo.png differ diff --git a/package-lock.json b/package-lock.json index 5f8d657475..380513176e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,8 +47,8 @@ "@ionic/angular": "^6.1.5", "@ionic/core": "^6.1.5", "@ionic/pwa-elements": "^3.1.1", - "@sentry/angular": "^7.1.1", - "@sentry/cli": "^2.1.0", + "@sentry/angular": "^7.118.0", + "@sentry/cli": "^2.33.0", "@sentry/tracing": "^7.1.1", "@types/google.maps": "^3.49.2", "@types/hammerjs": "^2.0.41", @@ -68,6 +68,8 @@ "jetifier": "^2.0.0", "launchdarkly-js-client-sdk": "^2.22.1", "lodash": "^4.17.21", + "mixpanel-browser": "^2.55.0", + "ng-otp-input": "^1.9.3", "ng2-pdf-viewer": "^9.0.0", "ngx-image-cropper": "^6.1.0", "ngx-mask": "^13.1.15", @@ -95,6 +97,7 @@ "@types/jasmine": "^4.0.3", "@types/jasminewd2": "^2.0.10", "@types/lodash": "^4.14.162", + "@types/mixpanel-browser": "^2.49.1", "@types/node": "^12.11.1", "@typescript-eslint/eslint-plugin": "5.3.0", "@typescript-eslint/parser": "5.3.0", @@ -4037,6 +4040,14 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz", + "integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.17" + } + }, "node_modules/@schematics/angular": { "version": "13.3.10", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.10.tgz", @@ -4053,50 +4064,244 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@sentry-internal/feedback": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.118.0.tgz", + "integrity": "sha512-IYOGRcqIqKJJpMwBBv+0JTu0FPpXnakJYvOx/XEa/SNyF5+l7b9gGEjUVWh1ok50kTLW/XPnpnXNAGQcoKHg+w==", + "dependencies": { + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.118.0.tgz", + "integrity": "sha512-XxHlCClvrxmVKpiZetFYyiBaPQNiojoBGFFVgbbWBIAPc+fWeLJ2BMoQEBjn/0NA/8u8T6lErK5YQo/eIx9+XQ==", + "dependencies": { + "@sentry/core": "7.118.0", + "@sentry/replay": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.118.0.tgz", + "integrity": "sha512-dERAshKlQLrBscHSarhHyUeGsu652bDTUN1FK0m4e3X48M3I5/s+0N880Qjpe5MprNLcINlaIgdQ9jkisvxjfw==", + "dependencies": { + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sentry/angular": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-7.27.0.tgz", - "integrity": "sha512-ZhY7m4oMWS2tXeskFdvQOr3xBihqQUVCI3H/dI5fhUZ7IFKA6VtUNlXhZJg6g1S2/K5xpkad0SvBiHk9KUBM3Q==", + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-7.118.0.tgz", + "integrity": "sha512-OujZYB89RRMHiLG39PDX07e0uL+GGr6YaJxEvq4XkUjkPiySlocUP1VKC0vUP8QjBP3K91KkcyfFAmFEBSQkJw==", "dependencies": { - "@sentry/browser": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0", - "tslib": "^2.0.0" + "@sentry/browser": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0", + "tslib": "^2.4.1" }, "engines": { "node": ">=8" }, "peerDependencies": { - "@angular/common": "10.x || 11.x || 12.x || 13.x || 14.x || 15.x", - "@angular/core": "10.x || 11.x || 12.x || 13.x || 14.x || 15.x", - "@angular/router": "10.x || 11.x || 12.x || 13.x || 14.x || 15.x", + "@angular/common": ">= 10.x <= 15.x", + "@angular/core": ">= 10.x <= 15.x", + "@angular/router": ">= 10.x <= 15.x", "rxjs": "^6.5.5 || ^7.x" } }, + "node_modules/@sentry/angular/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/angular/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/angular/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sentry/browser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.27.0.tgz", - "integrity": "sha512-6z+q+omLqmdEvy+9i4j7xzIT6zgmWJnXqEiLCURnE34KsPq6wr6Nij1XHsTlApMcohOpPlo+C3nMTmz+oYUf5w==", + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.118.0.tgz", + "integrity": "sha512-8onDOFV1VLEoBuqA5yaJeR3FF1JNuxr5C7p1oN3OwY724iTVqQnOLmZKZaSnHV3RkY67wKDGQkQIie14sc+42g==", "dependencies": { - "@sentry/core": "7.27.0", - "@sentry/replay": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0", - "tslib": "^1.9.3" + "@sentry-internal/feedback": "7.118.0", + "@sentry-internal/replay-canvas": "7.118.0", + "@sentry-internal/tracing": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/integrations": "7.118.0", + "@sentry/replay": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" }, "engines": { "node": ">=8" } }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "node_modules/@sentry/browser/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@sentry/cli": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.10.0.tgz", - "integrity": "sha512-VQnGXPQCqJyxirmUWkNznq3YYklDwDfbogok3lPHaL3r0bhgcNwt/bZxeycpaqk5I7myKNUYW8RG+F1YERODSw==", + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.33.0.tgz", + "integrity": "sha512-9MOzQy1UunVBhPOfEuO0JH2ofWAMmZVavTTR/Bo2CkJwI1qjyVF0UKLTXE3l4ujiJnFufOoBsVyKmYWXFerbCw==", "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", @@ -4110,6 +4315,123 @@ }, "engines": { "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.33.0", + "@sentry/cli-linux-arm": "2.33.0", + "@sentry/cli-linux-arm64": "2.33.0", + "@sentry/cli-linux-i686": "2.33.0", + "@sentry/cli-linux-x64": "2.33.0", + "@sentry/cli-win32-i686": "2.33.0", + "@sentry/cli-win32-x64": "2.33.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.33.0.tgz", + "integrity": "sha512-LQFvD7uCOQ2P/vYru7IBKqJDHwJ9Rr2vqqkdjbxe2YCQS/N3NPXvi3eVM9hDJ284oyV/BMZ5lrmVTuIicf/hhw==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.0.tgz", + "integrity": "sha512-gY1bFE7wjDJc7WiNq1AS0WrILqLLJUw6Ou4pFQS45KjaH3/XJ1eohHhGJNy/UBHJ/Gq32b/BA9vsnWTXClZJ7g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.0.tgz", + "integrity": "sha512-mR2ZhqpU8RBVGLF5Ji19iOmVznk1B7Bzg5VhA8bVPuKsQmFN/3SyqE87IPMhwKoAsSRXyctwmbAkKs4240fxGA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.0.tgz", + "integrity": "sha512-XPIy0XpqgAposHtWsy58qsX85QnZ8q0ktBuT4skrsCrLMzfhoQg4Ua+YbUr3RvE814Rt8Hzowx2ar2Rl3pyCyw==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.0.tgz", + "integrity": "sha512-qe1DdCUv4tmqS03s8RtCkEX9vCW2G+NgOxX6jZ5jN/sKDwjUlquljqo7JHUGSupkoXmymnNPm5By3rNr6VyNHg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.0.tgz", + "integrity": "sha512-VEXWtJ69C3b+kuSmXQJRwdQ0ypPGH88hpqyQuosbAOIqh/sv4g9B/u1ETHZc+whLdFDpPcTLVMbLDbXTGug0Yg==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.0.tgz", + "integrity": "sha512-GIUKysZ1xbSklY9h1aVaLMSYLsnMSd+JuwQLR+0wKw2wJC4O5kNCPFSGikhiOZM/kvh3GO1WnXNyazFp8nLAzw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, "node_modules/@sentry/core": { @@ -4130,20 +4452,94 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@sentry/integrations": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.118.0.tgz", + "integrity": "sha512-C2rR4NvIMjokF8jP5qzSf1o2zxDx7IeYnr8u15Kb2+HdZtX559owALR0hfgwnfeElqMhGlJBaKUWZ48lXJMzCQ==", + "dependencies": { + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sentry/replay": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.27.0.tgz", - "integrity": "sha512-Db1TBx4JZWWbsAXSzWfAE55d4ekpPspZheyF66j84xq8jaFxgmlMMO7wBD8P7CHuQ6VUkgwa4glMkcamj/sfSg==", + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.118.0.tgz", + "integrity": "sha512-boQfCL+1L/tSZ9Huwi00+VtU+Ih1Lcg8HtxBuAsBCJR9pQgUL5jp7ECYdTeeHyCh/RJO7JqV1CEoGTgohe10mA==", "dependencies": { - "@sentry/core": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0" + "@sentry-internal/tracing": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" }, "engines": { "node": ">=12" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "dependencies": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" }, - "peerDependencies": { - "@sentry/browser": ">=7.24.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "dependencies": { + "@sentry/types": "7.118.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@sentry/tracing": { @@ -4275,6 +4671,11 @@ "@types/node": "*" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" + }, "node_modules/@types/deep-freeze-strict": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.2.tgz", @@ -4397,6 +4798,12 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "node_modules/@types/mixpanel-browser": { + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.49.1.tgz", + "integrity": "sha512-W9VZxD7haNMenkRwXxPZBJLhED7Sx1l89nZsGcWi3WzdIk417k/KnpmfDFn2sEyL31G/h0rY1E6erAny+8ItOw==", + "dev": true + }, "node_modules/@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", @@ -4934,6 +5341,11 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5614,6 +6026,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9101,6 +9521,11 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -10148,8 +10573,7 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/immutable": { "version": "4.1.0", @@ -12033,6 +12457,22 @@ "node": ">= 12.13.0" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/localforage/node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -12595,6 +13035,19 @@ "node": ">= 8" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/mixpanel-browser": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.55.0.tgz", + "integrity": "sha512-7gn9DucCoEXFfJ5y28ycpb6BGguyGLsoPCrDmPXyEcmBQ7JarPI9ZkFDiX5xonnWAVR/+fix+22/lLvFb+IdWQ==", + "dependencies": { + "rrweb": "2.0.0-alpha.13" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -12638,10 +13091,15 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12756,6 +13214,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ng-otp-input": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/ng-otp-input/-/ng-otp-input-1.9.3.tgz", + "integrity": "sha512-QiAvOeTVPk8DiY2dnVstyCikWnsY70TXgsVl2iBgz7nFnNM5sdrgbVj8tP0usGznJowVRygCQZJ+hzvYLEhIOQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=6.0.0", + "@angular/core": ">=6.0.0" + } + }, "node_modules/ng2-pdf-viewer": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-9.1.3.tgz", @@ -13859,10 +14329,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -15447,6 +15916,64 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrdom": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.17.tgz", + "integrity": "sha512-b6caDiNcFO96Opp7TGdcVd4OLGSXu5dJe+A0IDiAu8mk7OmhqZCSDlgQdTKmdO5wMf4zPsUTgb8H/aNvR3kDHA==", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.17" + } + }, + "node_modules/rrweb": { + "version": "2.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.13.tgz", + "integrity": "sha512-a8GXOCnzWHNaVZPa7hsrLZtNZ3CGjiL+YrkpLo0TfmxGLhjNZbWY2r7pE06p+FcjFNlgUVTmFrSJbK3kO7yxvw==", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.13", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "fflate": "^0.4.4", + "mitt": "^3.0.0", + "rrdom": "^2.0.0-alpha.13", + "rrweb-snapshot": "^2.0.0-alpha.13" + } + }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz", + "integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==", + "dependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/rrweb-snapshot/node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -16147,10 +16674,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -20950,6 +21476,14 @@ "node-gyp-build": "^4.3.0" } }, + "@rrweb/types": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz", + "integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==", + "requires": { + "rrweb-snapshot": "^2.0.0-alpha.17" + } + }, "@schematics/angular": { "version": "13.3.10", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.10.tgz", @@ -20961,41 +21495,196 @@ "jsonc-parser": "3.0.0" } }, - "@sentry/angular": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-7.27.0.tgz", - "integrity": "sha512-ZhY7m4oMWS2tXeskFdvQOr3xBihqQUVCI3H/dI5fhUZ7IFKA6VtUNlXhZJg6g1S2/K5xpkad0SvBiHk9KUBM3Q==", + "@sentry-internal/feedback": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.118.0.tgz", + "integrity": "sha512-IYOGRcqIqKJJpMwBBv+0JTu0FPpXnakJYvOx/XEa/SNyF5+l7b9gGEjUVWh1ok50kTLW/XPnpnXNAGQcoKHg+w==", "requires": { - "@sentry/browser": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0", - "tslib": "^2.0.0" + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } } }, - "@sentry/browser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.27.0.tgz", - "integrity": "sha512-6z+q+omLqmdEvy+9i4j7xzIT6zgmWJnXqEiLCURnE34KsPq6wr6Nij1XHsTlApMcohOpPlo+C3nMTmz+oYUf5w==", + "@sentry-internal/replay-canvas": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.118.0.tgz", + "integrity": "sha512-XxHlCClvrxmVKpiZetFYyiBaPQNiojoBGFFVgbbWBIAPc+fWeLJ2BMoQEBjn/0NA/8u8T6lErK5YQo/eIx9+XQ==", "requires": { - "@sentry/core": "7.27.0", - "@sentry/replay": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0", - "tslib": "^1.9.3" + "@sentry/core": "7.118.0", + "@sentry/replay": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" }, "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } } } }, - "@sentry/cli": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.10.0.tgz", - "integrity": "sha512-VQnGXPQCqJyxirmUWkNznq3YYklDwDfbogok3lPHaL3r0bhgcNwt/bZxeycpaqk5I7myKNUYW8RG+F1YERODSw==", + "@sentry-internal/tracing": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.118.0.tgz", + "integrity": "sha512-dERAshKlQLrBscHSarhHyUeGsu652bDTUN1FK0m4e3X48M3I5/s+0N880Qjpe5MprNLcINlaIgdQ9jkisvxjfw==", + "requires": { + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } + } + }, + "@sentry/angular": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-7.118.0.tgz", + "integrity": "sha512-OujZYB89RRMHiLG39PDX07e0uL+GGr6YaJxEvq4XkUjkPiySlocUP1VKC0vUP8QjBP3K91KkcyfFAmFEBSQkJw==", "requires": { + "@sentry/browser": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0", + "tslib": "^2.4.1" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } + } + }, + "@sentry/browser": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.118.0.tgz", + "integrity": "sha512-8onDOFV1VLEoBuqA5yaJeR3FF1JNuxr5C7p1oN3OwY724iTVqQnOLmZKZaSnHV3RkY67wKDGQkQIie14sc+42g==", + "requires": { + "@sentry-internal/feedback": "7.118.0", + "@sentry-internal/replay-canvas": "7.118.0", + "@sentry-internal/tracing": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/integrations": "7.118.0", + "@sentry/replay": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } + } + }, + "@sentry/cli": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.33.0.tgz", + "integrity": "sha512-9MOzQy1UunVBhPOfEuO0JH2ofWAMmZVavTTR/Bo2CkJwI1qjyVF0UKLTXE3l4ujiJnFufOoBsVyKmYWXFerbCw==", + "requires": { + "@sentry/cli-darwin": "2.33.0", + "@sentry/cli-linux-arm": "2.33.0", + "@sentry/cli-linux-arm64": "2.33.0", + "@sentry/cli-linux-i686": "2.33.0", + "@sentry/cli-linux-x64": "2.33.0", + "@sentry/cli-win32-i686": "2.33.0", + "@sentry/cli-win32-x64": "2.33.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", @@ -21003,6 +21692,48 @@ "which": "^2.0.2" } }, + "@sentry/cli-darwin": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.33.0.tgz", + "integrity": "sha512-LQFvD7uCOQ2P/vYru7IBKqJDHwJ9Rr2vqqkdjbxe2YCQS/N3NPXvi3eVM9hDJ284oyV/BMZ5lrmVTuIicf/hhw==", + "optional": true + }, + "@sentry/cli-linux-arm": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.0.tgz", + "integrity": "sha512-gY1bFE7wjDJc7WiNq1AS0WrILqLLJUw6Ou4pFQS45KjaH3/XJ1eohHhGJNy/UBHJ/Gq32b/BA9vsnWTXClZJ7g==", + "optional": true + }, + "@sentry/cli-linux-arm64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.0.tgz", + "integrity": "sha512-mR2ZhqpU8RBVGLF5Ji19iOmVznk1B7Bzg5VhA8bVPuKsQmFN/3SyqE87IPMhwKoAsSRXyctwmbAkKs4240fxGA==", + "optional": true + }, + "@sentry/cli-linux-i686": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.0.tgz", + "integrity": "sha512-XPIy0XpqgAposHtWsy58qsX85QnZ8q0ktBuT4skrsCrLMzfhoQg4Ua+YbUr3RvE814Rt8Hzowx2ar2Rl3pyCyw==", + "optional": true + }, + "@sentry/cli-linux-x64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.0.tgz", + "integrity": "sha512-qe1DdCUv4tmqS03s8RtCkEX9vCW2G+NgOxX6jZ5jN/sKDwjUlquljqo7JHUGSupkoXmymnNPm5By3rNr6VyNHg==", + "optional": true + }, + "@sentry/cli-win32-i686": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.0.tgz", + "integrity": "sha512-VEXWtJ69C3b+kuSmXQJRwdQ0ypPGH88hpqyQuosbAOIqh/sv4g9B/u1ETHZc+whLdFDpPcTLVMbLDbXTGug0Yg==", + "optional": true + }, + "@sentry/cli-win32-x64": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.0.tgz", + "integrity": "sha512-GIUKysZ1xbSklY9h1aVaLMSYLsnMSd+JuwQLR+0wKw2wJC4O5kNCPFSGikhiOZM/kvh3GO1WnXNyazFp8nLAzw==", + "optional": true + }, "@sentry/core": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.27.0.tgz", @@ -21020,14 +21751,74 @@ } } }, + "@sentry/integrations": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.118.0.tgz", + "integrity": "sha512-C2rR4NvIMjokF8jP5qzSf1o2zxDx7IeYnr8u15Kb2+HdZtX559owALR0hfgwnfeElqMhGlJBaKUWZ48lXJMzCQ==", + "requires": { + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0", + "localforage": "^1.8.1" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } + } + }, "@sentry/replay": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.27.0.tgz", - "integrity": "sha512-Db1TBx4JZWWbsAXSzWfAE55d4ekpPspZheyF66j84xq8jaFxgmlMMO7wBD8P7CHuQ6VUkgwa4glMkcamj/sfSg==", + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.118.0.tgz", + "integrity": "sha512-boQfCL+1L/tSZ9Huwi00+VtU+Ih1Lcg8HtxBuAsBCJR9pQgUL5jp7ECYdTeeHyCh/RJO7JqV1CEoGTgohe10mA==", "requires": { - "@sentry/core": "7.27.0", - "@sentry/types": "7.27.0", - "@sentry/utils": "7.27.0" + "@sentry-internal/tracing": "7.118.0", + "@sentry/core": "7.118.0", + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + }, + "dependencies": { + "@sentry/core": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", + "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", + "requires": { + "@sentry/types": "7.118.0", + "@sentry/utils": "7.118.0" + } + }, + "@sentry/types": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", + "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==" + }, + "@sentry/utils": { + "version": "7.118.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", + "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "requires": { + "@sentry/types": "7.118.0" + } + } } }, "@sentry/tracing": { @@ -21144,6 +21935,11 @@ "@types/node": "*" } }, + "@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" + }, "@types/deep-freeze-strict": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.2.tgz", @@ -21266,6 +22062,12 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "@types/mixpanel-browser": { + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.49.1.tgz", + "integrity": "sha512-W9VZxD7haNMenkRwXxPZBJLhED7Sx1l89nZsGcWi3WzdIk417k/KnpmfDFn2sEyL31G/h0rY1E6erAny+8ItOw==", + "dev": true + }, "@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", @@ -21666,6 +22468,11 @@ "@xtuc/long": "4.2.2" } }, + "@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -22181,6 +22988,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -24709,6 +25521,11 @@ "pend": "~1.2.0" } }, + "fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -25497,8 +26314,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "immutable": { "version": "4.1.0", @@ -26924,6 +27740,24 @@ "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true }, + "localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "requires": { + "lie": "3.1.1" + }, + "dependencies": { + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "requires": { + "immediate": "~3.0.5" + } + } + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -27346,6 +28180,19 @@ "yallist": "^4.0.0" } }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "mixpanel-browser": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.55.0.tgz", + "integrity": "sha512-7gn9DucCoEXFfJ5y28ycpb6BGguyGLsoPCrDmPXyEcmBQ7JarPI9ZkFDiX5xonnWAVR/+fix+22/lLvFb+IdWQ==", + "requires": { + "rrweb": "2.0.0-alpha.13" + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -27380,10 +28227,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "native-run": { "version": "1.7.1", @@ -27470,6 +28316,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "ng-otp-input": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/ng-otp-input/-/ng-otp-input-1.9.3.tgz", + "integrity": "sha512-QiAvOeTVPk8DiY2dnVstyCikWnsY70TXgsVl2iBgz7nFnNM5sdrgbVj8tP0usGznJowVRygCQZJ+hzvYLEhIOQ==", + "requires": { + "tslib": "^2.3.0" + } + }, "ng2-pdf-viewer": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-9.1.3.tgz", @@ -28309,10 +29163,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "picomatch": { "version": "2.3.1", @@ -29416,6 +30269,49 @@ "glob": "^7.1.3" } }, + "rrdom": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.17.tgz", + "integrity": "sha512-b6caDiNcFO96Opp7TGdcVd4OLGSXu5dJe+A0IDiAu8mk7OmhqZCSDlgQdTKmdO5wMf4zPsUTgb8H/aNvR3kDHA==", + "requires": { + "rrweb-snapshot": "^2.0.0-alpha.17" + } + }, + "rrweb": { + "version": "2.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.13.tgz", + "integrity": "sha512-a8GXOCnzWHNaVZPa7hsrLZtNZ3CGjiL+YrkpLo0TfmxGLhjNZbWY2r7pE06p+FcjFNlgUVTmFrSJbK3kO7yxvw==", + "requires": { + "@rrweb/types": "^2.0.0-alpha.13", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "fflate": "^0.4.4", + "mitt": "^3.0.0", + "rrdom": "^2.0.0-alpha.13", + "rrweb-snapshot": "^2.0.0-alpha.13" + } + }, + "rrweb-snapshot": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz", + "integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==", + "requires": { + "postcss": "^8.4.38" + }, + "dependencies": { + "postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + } + } + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -29952,10 +30848,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, "source-map-loader": { "version": "3.0.1", diff --git a/package.json b/package.json index 52339d2ef0..1578c1eb68 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build", + "build": "ng build && npm run sentry:sourcemaps", "appflow": "sh build_appflow.sh", "test": "ng test --watch=false --browsers=ChromeHeadless", "test-basic": "ng test", @@ -17,7 +17,8 @@ "format:check": "prettier --write ./src", "prepare": "husky install", "lint:diff": "eslint -c .eslintrc.json $(git diff --name-only --diff-filter=ACMRTUXB origin/$GITHUB_BASE_REF | grep -E \"(.js$|.ts$|.tsx$)\")", - "lint:diff:local": "eslint --fix -c .eslintrc.json $(git diff --name-only --diff-filter=ACMRTUXB origin/master | grep -E '(.js$|.ts$|.tsx$)')" + "lint:diff:local": "eslint --fix -c .eslintrc.json $(git diff --name-only --diff-filter=ACMRTUXB origin/master | grep -E '(.js$|.ts$|.tsx$)')", + "sentry:sourcemaps": "sentry-cli sourcemaps inject --org fyle-technologies-private-limi --project fyle-mobile-app-prod ./www && sentry-cli sourcemaps upload --org fyle-technologies-private-limi --project fyle-mobile-app-prod ./www" }, "resolutions": { "webpack": "^5.0.0" @@ -62,8 +63,8 @@ "@ionic/angular": "^6.1.5", "@ionic/core": "^6.1.5", "@ionic/pwa-elements": "^3.1.1", - "@sentry/angular": "^7.1.1", - "@sentry/cli": "^2.1.0", + "@sentry/angular": "^7.118.0", + "@sentry/cli": "^2.33.0", "@sentry/tracing": "^7.1.1", "@types/google.maps": "^3.49.2", "@types/hammerjs": "^2.0.41", @@ -83,6 +84,8 @@ "jetifier": "^2.0.0", "launchdarkly-js-client-sdk": "^2.22.1", "lodash": "^4.17.21", + "mixpanel-browser": "^2.55.0", + "ng-otp-input": "^1.9.3", "ng2-pdf-viewer": "^9.0.0", "ngx-image-cropper": "^6.1.0", "ngx-mask": "^13.1.15", @@ -110,6 +113,7 @@ "@types/jasmine": "^4.0.3", "@types/jasminewd2": "^2.0.10", "@types/lodash": "^4.14.162", + "@types/mixpanel-browser": "^2.49.1", "@types/node": "^12.11.1", "@typescript-eslint/eslint-plugin": "5.3.0", "@typescript-eslint/parser": "5.3.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7dc9aefdec..c464c351da 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -105,7 +105,7 @@ export class AppComponent implements OnInit { style: Style.Default, }); - setTimeout(async () => await SplashScreen.hide(), 1000); + setTimeout(async () => await SplashScreen.hide(), 200); /* * Use the app's font size irrespective of the user's device font size. diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b981bc40c3..e162affd22 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -21,9 +21,10 @@ import { HAMMER_GESTURE_CONFIG, HammerGestureConfig } from '@angular/platform-br import { PAGINATION_SIZE, DEVICE_PLATFORM } from './constants'; import { Smartlook } from '@awesome-cordova-plugins/smartlook/ngx'; import { Capacitor } from '@capacitor/core'; +import { NgOtpInputModule } from 'ng-otp-input'; export class MyHammerConfig extends HammerGestureConfig { - overrides = { + overrides = { pinch: { enable: false }, rotate: { enable: false }, }; @@ -46,6 +47,7 @@ export const MIN_SCREEN_WIDTH = new InjectionToken( HttpClientJsonpModule, SharedModule, HammerModule, + NgOtpInputModule, ], providers: [ GooglePlus, @@ -74,7 +76,7 @@ export const MIN_SCREEN_WIDTH = new InjectionToken( ConfigService, { provide: APP_INITIALIZER, - useFactory: (configService: ConfigService) => () => configService.loadConfigurationData(), + useFactory: (configService: ConfigService) => (): Promise => configService.loadConfigurationData(), deps: [ConfigService, RouterAuthService, TokenService, SecureStorageService, StorageService, Sentry.TraceService], multi: true, }, diff --git a/src/app/auth/disabled/disabled.page.html b/src/app/auth/disabled/disabled.page.html index f78f04cfac..25c4b2f96c 100644 --- a/src/app/auth/disabled/disabled.page.html +++ b/src/app/auth/disabled/disabled.page.html @@ -1,25 +1,33 @@ - - - - - - - -
-
Account Disabled
- Stop -
- This account is no longer active. Please contact your company admin for details. +
+
+
+ +
+
+

Account disabled

+

+ This account is no longer active. Please contact your company admin for details. +

+
+
+
+ +
+ + Back to sign in +
+
- - - - - Incorrect Account? - Try Signing in Again - - - diff --git a/src/app/auth/disabled/disabled.page.scss b/src/app/auth/disabled/disabled.page.scss index 5fdb92d550..4b402c85cd 100644 --- a/src/app/auth/disabled/disabled.page.scss +++ b/src/app/auth/disabled/disabled.page.scss @@ -1,93 +1,80 @@ -$disabled-header: #220033; -$body-header: #000; -$body-subheader: #4a4a4a; -$password-icon: #b9beba; -$secondary-cta-border: #e0e0e0; +@import '../../../theme/colors.scss'; -.disabled { - &--header { - min-height: 140px; - } - - &--header-container { - min-height: 140px; - background-color: $disabled-header; - } +.disabled-user { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; - &--header-logo-container { - text-align: center; + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; } - &--header-logo { - max-width: 100px; + &__arrow-icon { + fill: $pure-white; + margin-right: 8px; } - &--body { - padding: 24px; - text-align: center; + &__cta-text { + font-size: 14px; + font-weight: 500; } - &--body-field { - width: 100%; + &__cta { + margin: 0 20px 24px 20px; } - &--body-header { - font-size: 24px; - margin-top: 16px; - margin-bottom: 8px; - color: $body-header; - font-weight: 700; + &__cta-content { + display: flex; + align-items: center; + justify-content: center; } - &--image { - max-width: 125px; - padding: 12px; + &__error-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $pale-pink; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; } - &--subtext { - font-size: 16px; - color: $body-subheader; - margin-top: 8px; - margin-bottom: 24px; + &__error-icon { + width: 45px; + height: 45px; } - &--primary-cta { - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - } + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; } - &--password-visibility-icon { - color: $password-icon; + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; } - &--secondary-cta { - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - letter-spacing: 1.6px; - border: 1px solid $secondary-cta-border; - } - margin-top: 24px; - } - - &--redirect { - margin: 16px; + &__content { + color: $black-light; text-align: center; - font-size: 16px; - a { - text-decoration: none; - } - } - - &--edit-email { - text-align: end; - } - - &--greyed { - color: grey; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; } } diff --git a/src/app/auth/new-password/new-password.module.ts b/src/app/auth/new-password/new-password.module.ts index ebe48b4b6e..452791efbb 100644 --- a/src/app/auth/new-password/new-password.module.ts +++ b/src/app/auth/new-password/new-password.module.ts @@ -17,6 +17,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { PopupComponent } from './popup/popup.component'; +import { SharedModule } from 'src/app/shared/shared.module'; @NgModule({ imports: [ @@ -30,6 +31,7 @@ import { PopupComponent } from './popup/popup.component'; ReactiveFormsModule, MatIconModule, MatButtonModule, + SharedModule, ], declarations: [NewPasswordPage, PopupComponent], }) diff --git a/src/app/auth/new-password/new-password.page.html b/src/app/auth/new-password/new-password.page.html index ad72afd5e7..4a6937ee91 100644 --- a/src/app/auth/new-password/new-password.page.html +++ b/src/app/auth/new-password/new-password.page.html @@ -1,133 +1,116 @@ - - - - - - - - -
-
Reset password
-
+ +
+
+
- - - - - Is between 12 to 32 characters -
-
- - - - - Contains atleast 1 uppercase character -
-
- - - - - Contains atleast 1 lowercase character -
-
- - - - - Contains atleast 1 number -
-
- - - - - Contains atleast 1 special character +
Reset password
+
+
+
+
+
New password
+
+ +
+ +
+
+ +
+
+ Password cannot be empty +
+
+ Please enter a valid password. +
+
+
+
+
Confirm new password
+
+ +
+ +
+
+
+
+ Password cannot be empty +
+
+ Passwords do not match +
+
+
+
-
Please enter a new password
-
- - - - {{hide ? 'visibility_off' : 'visibility'}} - - -
-
- + Reset password + +
+ + Back to sign in +
- - - - - Back to Sign in - - - diff --git a/src/app/auth/new-password/new-password.page.scss b/src/app/auth/new-password/new-password.page.scss index e1afee8662..5df595b64e 100644 --- a/src/app/auth/new-password/new-password.page.scss +++ b/src/app/auth/new-password/new-password.page.scss @@ -1,84 +1,114 @@ -$new-password-header: #220033; -$form-header: #000; -$form-subheader: #4a4a4a; -$password-icon: #b9beba; -$secondary-cta-border: #e0e0e0; -$flash-yellow: #fde081; -$error: #f00; -$offline-title: #4a4a4a; -$offline-sub-title: #ababab; +@import '../../../theme/colors.scss'; .new-password { - &--header { - min-height: 140px; + display: flex; + flex-direction: column; + padding: 24px 20px 44px 20px; + margin-top: 18px; + height: 100%; + justify-content: space-between; + + &__password-container { + display: flex; } - &--header-container { - min-height: 140px; - background-color: $new-password-header; + &__input-container { + margin-bottom: 24px; } - &--header-logo-container { - text-align: center; + &__error { + color: $red; + font-size: 12px; + display: flex; + align-items: center; } - &--header-logo { - max-width: 100px; + &__text { + border-bottom: 1px solid $grey-lighter; + + &__invalid { + border-bottom: 1px solid $red; + } } - &--form { - padding: 24px; + &__mandatory { + color: $brand-primary; + display: inline-block; + font-size: 14px; + font-weight: 400; } - &--form-field { - width: 100%; + &__text-label { + margin: 0 8px 0 0; + max-width: 90%; + min-width: 120px; + font-size: 12px; + color: $black-light; + line-height: 16px; + white-space: nowrap; + font-weight: 400; } - &--form-header { - font-size: 24px; - margin-top: 16px; - margin-bottom: 8px; - color: $form-header; - font-weight: 700; + &__text-input { + border: 0; + font-size: 14px; + font-weight: 400; + height: 18px; + line-height: 18px; + color: $blue-black; + width: 100%; + margin: 6px 0; + padding: 0; } - &--form-subheader { - font-size: 16px; - color: $form-subheader; - margin-top: 8px; - margin-bottom: 24px; + &__password-icon { + width: 20px; + height: 20px; + fill: $black-light; } - &--primary-cta { - margin-top: 12px; - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - } + &__password-icon-container { + display: flex; + align-items: center; } - &--password-rules { - padding: 14px 0; + &__form-header { + font-size: 20px; + position: relative; + margin-bottom: 24px; + color: $black; + font-weight: 500; } - &--save { + &__save { width: 100%; &__disabled { opacity: 0.2; } } - &--validation { - vertical-align: middle; - font-size: 16px; + &__back-icon { + margin-bottom: 16px; + width: 28px; + height: 28px; + } - &__correct { - color: green; - } + &__cta-text { + font-size: 14px; + font-weight: 500; + } - &__incorrect { - color: red; - } + &__arrow-icon { + fill: $pure-white; + margin-right: 6px; + } + + &__cta-secondary { + display: flex; + padding-top: 18px; + align-items: center; + justify-content: center; + color: $blue-black; + flex-direction: row; } } diff --git a/src/app/auth/new-password/new-password.page.spec.ts b/src/app/auth/new-password/new-password.page.spec.ts index f5db061720..c4da1499ba 100644 --- a/src/app/auth/new-password/new-password.page.spec.ts +++ b/src/app/auth/new-password/new-password.page.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { IonicModule, PopoverController } from '@ionic/angular'; +import { IonicModule } from '@ionic/angular'; import { NewPasswordPage } from './new-password.page'; import { AuthService } from 'src/app/core/services/auth.service'; @@ -9,14 +9,16 @@ import { TrackingService } from 'src/app/core/services/tracking.service'; import { DeviceService } from 'src/app/core/services/device.service'; import { LoginInfoService } from 'src/app/core/services/login-info.service'; import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; import { extendedDeviceInfoMockData } from 'src/app/core/mock-data/extended-device-info.data'; import { RouterTestingModule } from '@angular/router/testing'; import { getElementBySelector } from 'src/app/core/dom-helpers'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { PopupComponent } from './popup/popup.component'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; describe('NewPasswordPage', () => { let component: NewPasswordPage; @@ -24,19 +26,23 @@ describe('NewPasswordPage', () => { let authService: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; let loaderService: jasmine.SpyObj; - let popoverController: jasmine.SpyObj; let trackingService: jasmine.SpyObj; let deviceService: jasmine.SpyObj; let loginInfoService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; beforeEach(waitForAsync(() => { const authServiceSpy = jasmine.createSpyObj('AuthService', ['refreshEou']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['resetPassword']); const loaderServiceSpy = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); - const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const trackingServiceSpy = jasmine.createSpyObj('TrackingService', ['onSignin', 'resetPassword', 'eventTrack']); const deviceServiceSpy = jasmine.createSpyObj('DeviceService', ['getDeviceInfo']); const loginInfoServiceSpy = jasmine.createSpyObj('LoginInfoService', ['addLoginInfo']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ declarations: [NewPasswordPage], @@ -46,10 +52,12 @@ describe('NewPasswordPage', () => { { provide: AuthService, useValue: authServiceSpy }, { provide: RouterAuthService, useValue: routerAuthServiceSpy }, { provide: LoaderService, useValue: loaderServiceSpy }, - { provide: PopoverController, useValue: popoverControllerSpy }, { provide: TrackingService, useValue: trackingServiceSpy }, { provide: DeviceService, useValue: deviceServiceSpy }, { provide: LoginInfoService, useValue: loginInfoServiceSpy }, + { provide: MatSnackBar, useValue: matSnackBarSpy }, + { provide: SnackbarPropertiesService, useValue: snackbarPropertiesServiceSpy }, + { provide: Router, useValue: routerSpy }, { provide: ActivatedRoute, useValue: { @@ -67,10 +75,12 @@ describe('NewPasswordPage', () => { authService = TestBed.inject(AuthService) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; - popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; deviceService = TestBed.inject(DeviceService) as jasmine.SpyObj; loginInfoService = TestBed.inject(LoginInfoService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; fixture.detectChanges(); })); @@ -78,133 +88,23 @@ describe('NewPasswordPage', () => { expect(component).toBeTruthy(); }); - describe('ngOnInit():', () => { - it('should initialize the form and observables', () => { - component.ngOnInit(); - - expect(component.fg).toBeDefined(); - expect(component.lengthValidationDisplay$).toBeDefined(); - expect(component.uppercaseValidationDisplay$).toBeDefined(); - expect(component.numberValidationDisplay$).toBeDefined(); - expect(component.specialCharValidationDisplay$).toBeDefined(); - expect(component.lowercaseValidationDisplay$).toBeDefined(); - }); - - it('should validate password length of 12 characters', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('123456789012'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should validate password length of 32 characters', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('1234567890123456789012345678901'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate password length of less 12 characters', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('12345'); - expect(closeIcon).toBeDefined(); - }); - - it('should not validate password length of more 32 characters', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('12345678901234567890123456789012'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of an uppercase letter in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="uppercaseValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithUpperCase'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of an uppercase letter in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="uppercaseValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('passwordwithoutuppercase'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a number in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="numberValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithNumber123'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a number in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="numberValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithoutNumber'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a special character in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="specialcharValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWith@Special#Char'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a special character in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="specialcharValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithoutSpecialChar'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a lowercase letter in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lowercaseValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithLowerCase'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a lowercase letter in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lowercaseValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PASSWORDWITHOUTLOWERCASE'); - expect(closeIcon).toBeDefined(); - }); - }); - describe('changePassword', () => { const passwordValue = 'DummyPassword@123'; const refreshToken = 'token123'; const resetPasswordRes = { ...apiEouRes, refresh_token: refreshToken }; it('should change the password and show success message on success', fakeAsync(() => { + const message = 'Password changed successfully'; spyOn(component, 'trackLoginInfo'); routerAuthService.resetPassword.and.returnValue(of(resetPasswordRes)); authService.refreshEou.and.returnValue(of(apiEouRes)); - const popoverSpy = jasmine.createSpyObj('HTMLIonPopoverElement', ['present']); - popoverController.create.and.returnValue(popoverSpy); deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData)); loaderService.showLoader.and.resolveTo(); loaderService.hideLoader.and.resolveTo(); component.fg.controls.password.setValue(passwordValue); fixture.detectChanges(); - const newPasswordButton = getElementBySelector(fixture, '#new-password--btn-sign-in') as HTMLButtonElement; + const newPasswordButton = getElementBySelector(fixture, '.btn-primary') as HTMLButtonElement; newPasswordButton.click(); tick(500); @@ -215,27 +115,23 @@ describe('NewPasswordPage', () => { expect(trackingService.onSignin).toHaveBeenCalledOnceWith('ajain@fyle.in'); expect(trackingService.resetPassword).toHaveBeenCalledTimes(1); expect(component.trackLoginInfo).toHaveBeenCalledTimes(1); - expect(popoverController.create).toHaveBeenCalledOnceWith({ - component: PopupComponent, - componentProps: { - header: 'Password changed successfully', - route: ['/', 'auth', 'switch_org'], - }, - cssClass: 'dialog-popover', + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledOnceWith('success', { message }); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...snackbarPropertiesService.setSnackbarProperties('success', { message }), + panelClass: ['msb-success'], }); })); it('should show error message on failure', fakeAsync(() => { + const message = 'Something went wrong. Please try after some time.'; spyOn(component, 'trackLoginInfo'); routerAuthService.resetPassword.and.rejectWith(); - const popoverSpy = jasmine.createSpyObj('HTMLIonPopoverElement', ['present']); - popoverController.create.and.returnValue(popoverSpy); loaderService.showLoader.and.resolveTo(); loaderService.hideLoader.and.resolveTo(); component.fg.controls.password.setValue(passwordValue); fixture.detectChanges(); - const newPasswordButton = getElementBySelector(fixture, '#new-password--btn-sign-in') as HTMLButtonElement; + const newPasswordButton = getElementBySelector(fixture, '.btn-primary') as HTMLButtonElement; newPasswordButton.click(); tick(500); @@ -246,13 +142,9 @@ describe('NewPasswordPage', () => { expect(trackingService.onSignin).not.toHaveBeenCalled(); expect(trackingService.resetPassword).not.toHaveBeenCalled(); expect(component.trackLoginInfo).not.toHaveBeenCalled(); - expect(popoverController.create).toHaveBeenCalledOnceWith({ - component: PopupComponent, - componentProps: { - header: 'Setting new password failed. Please try again later.', - route: ['/', 'auth', 'sign_in'], - }, - cssClass: 'dialog-popover', + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...snackbarPropertiesService.setSnackbarProperties('failure', { message }), + panelClass: ['msb-failure'], }); })); }); @@ -270,4 +162,81 @@ describe('NewPasswordPage', () => { expect(trackingService.eventTrack).toHaveBeenCalledOnceWith('Added Login Info', { label: '5.50.0' }); expect(loginInfoService.addLoginInfo).toHaveBeenCalledOnceWith('5.50.0', mockDate); })); + + it('redirectToSignIn(): should navigate to the sign-in page', () => { + component.redirectToSignIn(); + // @ts-ignore + expect(component.router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'sign_in']); // Should navigate to the correct route + }); + + describe('checkPasswordValidity():', () => { + it('should return null when isPasswordValid is true', () => { + component.isPasswordValid = true; + + const result = component.checkPasswordValidity(); + + expect(result).toBeNull(); // No errors + }); + + it('should return an error object when isPasswordValid is false', () => { + component.isPasswordValid = false; + + const result = component.checkPasswordValidity(); + + expect(result).toEqual({ invalidPassword: true }); // Error object + }); + }); + + describe('validatePasswordEquality():', () => { + it('should return null when password and confirmPassword match', () => { + component.fg.controls.password.setValue('StrongPassword@123'); + component.fg.controls.confirmPassword.setValue('StrongPassword@123'); + + const result = component.validatePasswordEquality(); + + expect(result).toBeNull(); // No errors + }); + + it('should return an error object when password and confirmPassword do not match', () => { + component.fg.controls.password.setValue('StrongPassword@123'); + component.fg.controls.confirmPassword.setValue('DifferentPassword@123'); + + const result = component.validatePasswordEquality(); + + expect(result).toEqual({ passwordMismatch: true }); + }); + + it('should return null when password or confirmPassword is empty', () => { + component.fg.controls.password.setValue(''); + component.fg.controls.confirmPassword.setValue(''); + + const result = component.validatePasswordEquality(); + + expect(result).toBeNull(); + }); + }); + + describe('onPasswordValid():', () => { + it('should set isPasswordValid to true when called with true', () => { + component.onPasswordValid(true); + expect(component.isPasswordValid).toBeTrue(); + }); + + it('should set isPasswordValid to false when called with false', () => { + component.onPasswordValid(false); + expect(component.isPasswordValid).toBeFalse(); + }); + }); + + describe('setPasswordTooltip():', () => { + it('should set showPasswordTooltip to true when called with true', () => { + component.setPasswordTooltip(true); + expect(component.showPasswordTooltip).toBeTrue(); + }); + + it('should set showPasswordTooltip to false when called with false', () => { + component.setPasswordTooltip(false); + expect(component.showPasswordTooltip).toBeFalse(); + }); + }); }); diff --git a/src/app/auth/new-password/new-password.page.ts b/src/app/auth/new-password/new-password.page.ts index 0d5295e798..57530edce8 100644 --- a/src/app/auth/new-password/new-password.page.ts +++ b/src/app/auth/new-password/new-password.page.ts @@ -1,16 +1,17 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { finalize, map, switchMap, tap } from 'rxjs/operators'; +import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { finalize, switchMap, tap } from 'rxjs/operators'; import { from, Observable } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { LoaderService } from 'src/app/core/services/loader.service'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { AuthService } from 'src/app/core/services/auth.service'; -import { PopoverController } from '@ionic/angular'; -import { PopupComponent } from './popup/popup.component'; import { TrackingService } from '../../core/services/tracking.service'; import { DeviceService } from '../../core/services/device.service'; import { LoginInfoService } from '../../core/services/login-info.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; @Component({ selector: 'app-new-password', @@ -30,7 +31,15 @@ export class NewPasswordPage implements OnInit { lowercaseValidationDisplay$: Observable; - hide = false; + isPasswordValid = false; + + hide = true; + + hideConfirmPassword = true; + + showPasswordTooltip = false; + + isLoading = false; constructor( private fb: FormBuilder, @@ -38,54 +47,27 @@ export class NewPasswordPage implements OnInit { private loaderService: LoaderService, private routerAuthService: RouterAuthService, private authService: AuthService, - private popoverController: PopoverController, private trackingService: TrackingService, private deviceService: DeviceService, - private loginInfoService: LoginInfoService + private loginInfoService: LoginInfoService, + private router: Router, + private matSnackBar: MatSnackBar, + private snackbarPropertiesService: SnackbarPropertiesService ) {} - ngOnInit() { + ngOnInit(): void { this.fg = this.fb.group({ - password: [ - '', - Validators.compose([ - Validators.required, - Validators.minLength(12), - Validators.maxLength(32), - Validators.pattern(/[A-Z]/), - Validators.pattern(/[a-z]/), - Validators.pattern(/[0-9]/), - Validators.pattern(/[!@#$%^&*()+\-:;<=>{}|~?]/), - ]), - ], + password: ['', [Validators.required, this.checkPasswordValidity]], + confirmPassword: ['', [Validators.required, this.validatePasswordEquality]], }); - - this.lengthValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => password && password.length >= 12 && password.length <= 32) - ); - - this.uppercaseValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[A-Z]/.test(password)) - ); - - this.numberValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[0-9]/.test(password)) - ); - this.specialCharValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[!@#$%^&*()+\-:;<=>{}|~?]/.test(password)) - ); - - this.lowercaseValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[a-z]/.test(password)) - ); } - changePassword() { - const refreshToken = this.activatedRoute.snapshot.params.refreshToken; - + changePassword(): void { + const refreshToken = this.activatedRoute.snapshot.params.refreshToken as string; + this.isLoading = true; from(this.loaderService.showLoader()) .pipe( - switchMap(() => this.routerAuthService.resetPassword(refreshToken, this.fg.controls.password.value)), + switchMap(() => this.routerAuthService.resetPassword(refreshToken, this.fg.controls.password.value as string)), switchMap(() => this.authService.refreshEou()), tap(async (eou) => { const email = eou.us.email; @@ -93,39 +75,65 @@ export class NewPasswordPage implements OnInit { this.trackingService.resetPassword(); await this.trackLoginInfo(); }), - finalize(() => from(this.loaderService.hideLoader())) + finalize(() => { + this.isLoading = false; + return from(this.loaderService.hideLoader()); + }) ) .subscribe( - async () => { - const popup = await this.popoverController.create({ - component: PopupComponent, - componentProps: { - header: 'Password changed successfully', - route: ['/', 'auth', 'switch_org'], - }, - cssClass: 'dialog-popover', + () => { + const toastMessageData = { + message: 'Password changed successfully', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarPropertiesService.setSnackbarProperties('success', toastMessageData), + panelClass: ['msb-success'], }); - - await popup.present(); + this.router.navigate(['/', 'auth', 'switch_org']); }, - async () => { - const popup = await this.popoverController.create({ - component: PopupComponent, - componentProps: { - header: 'Setting new password failed. Please try again later.', - route: ['/', 'auth', 'sign_in'], - }, - cssClass: 'dialog-popover', + () => { + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarPropertiesService.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], }); - - await popup.present(); + this.router.navigate(['/', 'auth', 'sign_in']); } ); } - async trackLoginInfo() { + async trackLoginInfo(): Promise { const deviceInfo = await this.deviceService.getDeviceInfo().toPromise(); this.trackingService.eventTrack('Added Login Info', { label: deviceInfo.appVersion }); await this.loginInfoService.addLoginInfo(deviceInfo.appVersion, new Date()); } + + onPasswordValid(isValid: boolean): void { + this.isPasswordValid = isValid; + this.fg.controls.password.updateValueAndValidity(); + this.fg.controls.confirmPassword.updateValueAndValidity(); + } + + redirectToSignIn(): void { + this.router.navigate(['/', 'auth', 'sign_in']); + } + + setPasswordTooltip(value: boolean): void { + this.showPasswordTooltip = value; + } + + checkPasswordValidity = (): ValidationErrors => (this.isPasswordValid ? null : { invalidPassword: true }); + + validatePasswordEquality = (): ValidationErrors => { + if (!this.fg) { + return null; + } + const password = this.fg.controls.password.value as string; + const confirmPassword = this.fg.controls.confirmPassword.value as string; + return password === confirmPassword ? null : { passwordMismatch: true }; + }; } diff --git a/src/app/auth/pending-verification/pending-verification.page.html b/src/app/auth/pending-verification/pending-verification.page.html index 0e31f3bb36..317f9d0cad 100644 --- a/src/app/auth/pending-verification/pending-verification.page.html +++ b/src/app/auth/pending-verification/pending-verification.page.html @@ -1,26 +1,96 @@ - - - + +
+
+
+
+ +
+
+
The invitation has expired
+
+ Enter your registered email address to receive a new invitation and set up your account on Fyle. +
+
+
+
Registered email
+ +
+ Please enter a valid email. +
+
+
+
+ + Send invite + +
+ + Back to Sign In +
+
+
+
+
+
+ +
+
+

Invitation link sent

+

+ A new invitation link has been sent to your registered email address. Check your inbox to continue setting + up your account. +

+
+
+
+ +
+ + Back to sign in +
+
+
+
+
+
diff --git a/src/app/auth/pending-verification/pending-verification.page.scss b/src/app/auth/pending-verification/pending-verification.page.scss new file mode 100644 index 0000000000..18f0553f15 --- /dev/null +++ b/src/app/auth/pending-verification/pending-verification.page.scss @@ -0,0 +1,203 @@ +@import '../../../theme/colors.scss'; + +.pending-verification { + height: 100%; + margin-top: 18px; + padding-top: 28px; + padding-bottom: 20px; + + &__send-invite { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: 0 20px 24px 20px; + } + + &__form-container { + display: flex; + flex-direction: column; + padding-top: 24px; + justify-content: flex-start; + height: 100%; + } + + &__error-icon { + width: 45px; + height: 45px; + fill: $red; + } + + &__error-icon-container { + width: 40px; + height: 40px; + border-radius: 8px; + background: $pale-pink; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + padding: 10px; + } + + &__page-header { + font-size: 20px; + margin: 0 0 10px; + position: relative; + margin-bottom: 8px; + font-weight: 500; + } + + &__disabled { + opacity: 0.2; + } + + &__sub-header { + font-size: 14px; + margin: 0 0 10px; + position: relative; + margin-bottom: 32px; + color: $black-light; + line-height: 1.28; + } + + &__error-message { + color: $red; + font-size: 12px; + display: flex; + align-items: center; + } + + &__input-container { + &__label { + margin: 0 8px 0 0; + font-size: 12px; + color: $black-light; + line-height: 1.3; + font-weight: 400; + } + + &__input { + border: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.3; + color: $blue-black; + width: 100%; + padding: 6px 0; + border-bottom: 1px solid $grey-lighter; + } + + &__input:focus { + border-bottom: 1px solid $blue-black; + } + } + + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; + } + + &__arrow-icon { + fill: $pure-white; + margin-right: 6px; + } + + &__cta-text { + font-size: 14px; + font-weight: 500; + } + + &__cta-content { + display: flex; + align-items: center; + justify-content: center; + } + + &__success-message { + height: 100%; + display: flex; + flex-direction: column; + } + + &__success-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $success-bg; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + } + + &__success-icon { + width: 45px; + height: 45px; + fill: $green; + } + + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; + } + + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; + } + + &__resend-text { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + color: $black-light; + font-weight: 500; + gap: 2px; + + &__resend-link { + color: $brand-primary; + } + + &__spinner-icon { + color: $brand-primary; + height: 12px; + } + } + + &__content { + color: $black-light; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; + + &__reset-email { + font-weight: 500; + } + } + &__cta-secondary { + display: flex; + padding-top: 18px; + flex-direction: column; + justify-content: center; + align-items: center; + color: $blue-black; + display: flex; + flex-direction: row; + } +} diff --git a/src/app/auth/pending-verification/pending-verification.page.spec.ts b/src/app/auth/pending-verification/pending-verification.page.spec.ts index 17271dc667..604f6b7f00 100644 --- a/src/app/auth/pending-verification/pending-verification.page.spec.ts +++ b/src/app/auth/pending-verification/pending-verification.page.spec.ts @@ -1,48 +1,76 @@ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { PendingVerificationPage } from './pending-verification.page'; -import { PageState } from 'src/app/core/models/page-state.enum'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { of, throwError } from 'rxjs'; import { authResData1 } from 'src/app/core/mock-data/auth-reponse.data'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; +import { HttpErrorResponse } from '@angular/common/http'; +import { getElementRef } from 'src/app/core/dom-helpers'; describe('PendingVerificationPage', () => { let component: PendingVerificationPage; let fixture: ComponentFixture; let router: jasmine.SpyObj; - let activatedRoute: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let formBuilder: jasmine.SpyObj; + let fb: FormBuilder; beforeEach(waitForAsync(() => { const routerSpy = jasmine.createSpyObj('Router', ['navigate']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['resendVerificationLink']); - + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ declarations: [PendingVerificationPage], - imports: [IonicModule.forRoot()], + imports: [IonicModule.forRoot(), RouterTestingModule, RouterModule, FormsModule, ReactiveFormsModule], providers: [ - { provide: Router, useValue: routerSpy }, - { - provide: ActivatedRoute, - useValue: { snapshot: { params: { orgId: 'orNVthTo2Zyo' } } }, - }, + FormBuilder, { provide: RouterAuthService, useValue: routerAuthServiceSpy, }, + { + provide: Router, + useValue: routerSpy, + }, + { + provide: MatSnackBar, + useValue: matSnackBarSpy, + }, + { + provide: SnackbarPropertiesService, + useValue: snackbarPropertiesServiceSpy, + }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { email: 'aastha.b@fyle.in' } } }, + }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(PendingVerificationPage); component = fixture.componentInstance; router = TestBed.inject(Router) as jasmine.SpyObj; - activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + fb = TestBed.inject(FormBuilder); + activatedRoute.snapshot.params.orgId = 'orNVthTo2Zyo'; + component.fg = fb.group({ + email: ['', Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); fixture.detectChanges(); })); @@ -50,33 +78,6 @@ describe('PendingVerificationPage', () => { expect(component).toBeTruthy(); }); - it('should set hasTokenExpired to true if snapshot.params.hasTokenExpired is defined and true on ionViewWillEnter()', () => { - activatedRoute.snapshot.params.hasTokenExpired = true; - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(true); - expect(pageTitle).toEqual('Verification link expired'); - }); - - it('should set hasTokenExpired to false if snapshot.params.hasTokenExpired is defined and false on ionViewWillEnter()', () => { - activatedRoute.snapshot.params.hasTokenExpired = false; - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(false); - expect(pageTitle).toEqual('Please verify your email'); - }); - - it('should set hasTokenExpired to false and currentPageState to notSent if snapshot.params.hasTokenExpired is not defined on ionViewWillEnter()', () => { - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(false); - expect(component.currentPageState).toEqual(PageState.notSent); - expect(pageTitle).toEqual('Please verify your email'); - }); - it('resendVerificationLink(): should call routerAuthService and set PageState to success if API is successful', fakeAsync(() => { const data = { cluster_domain: authResData1.cluster_domain, @@ -86,36 +87,80 @@ describe('PendingVerificationPage', () => { tick(1000); expect(routerAuthService.resendVerificationLink).toHaveBeenCalledOnceWith('ajain@fyle.in', 'orNVthTo2Zyo'); expect(component.isLoading).toBeFalse(); - expect(component.currentPageState).toEqual(PageState.success); })); it('resendVerificationLink(): should call routerAuthService and call handleError if API is unsuccessful', fakeAsync(() => { - const error = new Error('An Error Occured'); + const error = { status: 500 } as HttpErrorResponse; routerAuthService.resendVerificationLink.and.returnValue(throwError(() => error)); spyOn(component, 'handleError'); component.resendVerificationLink('ajain@fyle.in'); tick(1000); expect(routerAuthService.resendVerificationLink).toHaveBeenCalledOnceWith('ajain@fyle.in', 'orNVthTo2Zyo'); expect(component.isLoading).toBeTrue(); - expect(component.currentPageState).not.toEqual(PageState.success); expect(component.handleError).toHaveBeenCalledOnceWith(error); })); - it('handleError(); should navigate to auth/disabled if status code is 422', () => { - const error = { - status: 422, - }; - component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); - expect(component.currentPageState).not.toEqual(PageState.failure); + describe('handleError():', () => { + it('handleError(); should navigate to auth/disabled if status code is 422', () => { + const error = { + status: 422, + } as HttpErrorResponse; + component.handleError(error); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); + }); + + it('should display error message on other errors', () => { + const error = { status: 401 } as HttpErrorResponse; + const props = { + panelClass: ['msb-failure'], + }; + + matSnackBar.openFromComponent.and.callThrough(); + + component.handleError(error); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...props, + panelClass: ['msb-failure'], + }); + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledTimes(1); + }); }); - it('handleError(); should set pagestate to failure if status code', () => { - const error = { - status: 422, - }; - component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); - expect(component.currentPageState).not.toEqual(PageState.failure); + it('onGotoSignInClick(): should navigate to sign-in page', () => { + component.onGotoSignInClick(); + expect(router.navigate).toHaveBeenCalledWith(['/', 'auth', 'sign_in']); + }); + + describe('template', () => { + it('should render the form for entering email', () => { + component.isInvitationLinkSent = false; + fixture.detectChanges(); + + const formElement = fixture.debugElement.query(By.css('.pending-verification__form-container')); + expect(formElement).toBeTruthy(); + }); + + it('should display validation error for invalid email input', () => { + component.isInvitationLinkSent = false; + const emailControl = component.fg.controls.email; + emailControl.setValue('invalid-email'); + emailControl.markAsTouched(); + fixture.detectChanges(); + + const errorElement = getElementRef(fixture, '.pending-verification__error-message'); + expect(errorElement.nativeElement.textContent).toContain('Please enter a valid email.'); + }); + + it('should call resendVerificationLink with correct email when button is clicked', () => { + component.isInvitationLinkSent = false; + spyOn(component, 'resendVerificationLink'); + component.fg.controls.email.setValue('test@example.com'); + fixture.detectChanges(); + + const buttonElement = fixture.debugElement.query(By.css('ion-button')); + buttonElement.triggerEventHandler('click', null); + + expect(component.resendVerificationLink).toHaveBeenCalledWith('test@example.com'); + }); }); }); diff --git a/src/app/auth/pending-verification/pending-verification.page.ts b/src/app/auth/pending-verification/pending-verification.page.ts index 814941636a..cf54a1723b 100644 --- a/src/app/auth/pending-verification/pending-verification.page.ts +++ b/src/app/auth/pending-verification/pending-verification.page.ts @@ -1,51 +1,71 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { tap } from 'rxjs/operators'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; -import { PageState } from 'src/app/core/models/page-state.enum'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pending-verification', templateUrl: './pending-verification.page.html', + styleUrls: ['./pending-verification.page.scss'], }) -export class PendingVerificationPage implements OnInit { - currentPageState: PageState; - +export class PendingVerificationPage { isLoading = false; - hasTokenExpired = false; + isInvitationLinkSent = false; + + fg: FormGroup; constructor( + private formBuilder: FormBuilder, private routerAuthService: RouterAuthService, private router: Router, - private activatedRoute: ActivatedRoute + private activatedRoute: ActivatedRoute, + private matSnackBar: MatSnackBar, + private snackbarProperties: SnackbarPropertiesService ) {} - ngOnInit() {} - - ionViewWillEnter() { - this.hasTokenExpired = this.activatedRoute.snapshot.params.hasTokenExpired || false; - this.currentPageState = PageState.notSent; + ionViewWillEnter(): void { + this.fg = this.formBuilder.group({ + email: ['', Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); } - resendVerificationLink(email: string) { + resendVerificationLink(email: string): void { this.isLoading = true; - const orgId = this.activatedRoute.snapshot.params.orgId; + const orgId = this.activatedRoute.snapshot.params.orgId as string; this.routerAuthService .resendVerificationLink(email, orgId) .pipe(tap(() => (this.isLoading = false))) .subscribe({ - next: () => (this.currentPageState = PageState.success), - error: (err) => this.handleError(err), + next: () => { + this.isInvitationLinkSent = true; + }, + error: (err: HttpErrorResponse) => this.handleError(err), }); } - handleError(err: any) { + handleError(err: HttpErrorResponse): void { if (err.status === 422) { this.router.navigate(['/', 'auth', 'disabled']); } else { - this.currentPageState = PageState.failure; + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], + }); } } + + onGotoSignInClick(): void { + this.router.navigate(['/', 'auth', 'sign_in']); + } } diff --git a/src/app/auth/reset-password/reset-password.page.html b/src/app/auth/reset-password/reset-password.page.html index e7ff210805..fb032a0b3e 100644 --- a/src/app/auth/reset-password/reset-password.page.html +++ b/src/app/auth/reset-password/reset-password.page.html @@ -1,11 +1,94 @@ - + +
+
+
+ +
+
Forgot password
+
+ Please enter your registered email address to receive instructions for resetting your password +
+
+
+
Registered email
+ +
+ Enter an email address. +
+
+
+
+ + Request reset link + +
+
+
+
+
+ +
+
+

Check your email

+

+ We've sent password recovery instructions to + {{resetEmail}} +

+
+
+ Didnโ€™t receive the email? + Resend + +
+
+
+ +
+ + Back to sign in +
+
+
+
+
+
diff --git a/src/app/auth/reset-password/reset-password.page.scss b/src/app/auth/reset-password/reset-password.page.scss new file mode 100644 index 0000000000..1e73b237ee --- /dev/null +++ b/src/app/auth/reset-password/reset-password.page.scss @@ -0,0 +1,174 @@ +@import '../../../theme/colors.scss'; + +.forgot-password { + height: 100%; + margin-top: 18px; + padding-bottom: 20px; + + &__form-container { + display: flex; + flex-direction: column; + padding: 24px 20px; + justify-content: space-between; + height: 100%; + } + + &__page-header { + font-size: 20px; + margin: 0 0 10px; + position: relative; + margin-bottom: 6px; + font-weight: 500; + } + + &__disabled { + opacity: 0.2; + } + + &__sub-header { + font-size: 14px; + margin: 0 0 10px; + position: relative; + margin-bottom: 24px; + color: $black-light; + } + + &__error-message { + color: $red; + display: flex; + align-items: center; + } + + &__input-container { + &__label { + margin: 0 8px 0 0; + font-size: 12px; + color: $black-light; + line-height: 1.3; + font-weight: 400; + } + + &__input { + border: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.3; + color: $blue-black; + width: 100%; + padding: 6px 0; + border-bottom: 1px solid $grey-lighter; + } + + &__input:focus { + border-bottom: 1px solid $blue-black; + } + } + + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; + } + + &__arrow-icon { + fill: $pure-white; + margin-right: 8px; + } + + &__cta-text { + font-size: 14px; + font-weight: 500; + } + + &__cta { + margin: 0 20px 24px 20px; + } + + &__cta-content { + display: flex; + align-items: center; + justify-content: center; + } + + &__success-message { + height: 100%; + display: flex; + flex-direction: column; + } + + &__success-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $success-bg; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + } + + &__success-icon { + width: 45px; + height: 45px; + fill: $green; + } + + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; + } + + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; + } + + &__resend-text { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + color: $black-light; + font-weight: 500; + gap: 2px; + + &__resend-link { + color: $brand-primary; + } + + &__spinner-icon { + color: $brand-primary; + height: 12px; + } + } + + &__content { + color: $black-light; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; + + &__reset-email { + font-weight: 500; + } + } + + &__back-icon { + margin-bottom: 16px; + width: 28px; + height: 28px; + } +} diff --git a/src/app/auth/reset-password/reset-password.page.spec.ts b/src/app/auth/reset-password/reset-password.page.spec.ts index 55a30ea298..a1d25fbc7d 100644 --- a/src/app/auth/reset-password/reset-password.page.spec.ts +++ b/src/app/auth/reset-password/reset-password.page.spec.ts @@ -1,54 +1,40 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ResetPasswordPage } from './reset-password.page'; +import { ReactiveFormsModule, FormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { IonicModule } from '@ionic/angular'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; -import { Router, RouterModule } from '@angular/router'; -import { Location } from '@angular/common'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; import { PageState } from 'src/app/core/models/page-state.enum'; +import { getElementRef } from 'src/app/core/dom-helpers'; +import { DebugElement } from '@angular/core'; import { of, throwError } from 'rxjs'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; describe('ResetPasswordPage', () => { let component: ResetPasswordPage; let fixture: ComponentFixture; let router: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; - let location: jasmine.SpyObj; - - @Component({ - selector: 'app-send-email', - template: '
', - }) - class MockSendEmailComponent { - @Input() title: string; - - @Input() content: string; - - @Input() subcontent: string; - - @Input() ctaText: string; - - @Input() successTitle: string; - - @Input() successContent: string; - - @Input() sendEmailPageState: PageState; - - @Input() isLoading: boolean; - - @Output() sendEmail = new EventEmitter(); - } + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let formBuilder: jasmine.SpyObj; + let fb: FormBuilder; beforeEach(waitForAsync(() => { const routerSpy = jasmine.createSpyObj('Router', ['navigate']); - const locationSpy = jasmine.createSpyObj('Location', ['path']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['sendResetPassword']); + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ - declarations: [ResetPasswordPage, MockSendEmailComponent], + declarations: [ResetPasswordPage], imports: [IonicModule.forRoot(), RouterTestingModule, RouterModule, FormsModule, ReactiveFormsModule], providers: [ + FormBuilder, { provide: RouterAuthService, useValue: routerAuthServiceSpy, @@ -58,8 +44,16 @@ describe('ResetPasswordPage', () => { useValue: routerSpy, }, { - provide: Location, - useValue: locationSpy, + provide: MatSnackBar, + useValue: matSnackBarSpy, + }, + { + provide: SnackbarPropertiesService, + useValue: snackbarPropertiesServiceSpy, + }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { email: 'aastha.b@fyle.in' } } }, }, ], }).compileComponents(); @@ -67,12 +61,18 @@ describe('ResetPasswordPage', () => { fixture = TestBed.createComponent(ResetPasswordPage); component = fixture.componentInstance; router = TestBed.inject(Router) as jasmine.SpyObj; - location = TestBed.inject(Location) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + fb = TestBed.inject(FormBuilder); + component.fg = fb.group({ + email: [Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); fixture.detectChanges(); })); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); @@ -81,15 +81,89 @@ describe('ResetPasswordPage', () => { expect(component.currentPageState).toEqual(PageState.notSent); }); + describe('template', () => { + it('should render the form in "notSent" state', () => { + component.currentPageState = component.PageState.notSent; + fixture.detectChanges(); + + const formElement = fixture.debugElement.query(By.css('.forgot-password__form-container')); + expect(formElement).toBeTruthy(); + }); + + it('should render the success message in "success" state', () => { + component.currentPageState = component.PageState.success; + component.resetEmail = 'test@example.com'; + fixture.detectChanges(); + + const successMessageElement = fixture.debugElement.query(By.css('.forgot-password__success-message')); + expect(successMessageElement).toBeTruthy(); + + const emailElement = fixture.debugElement.query(By.css('.forgot-password__content__reset-email')); + expect(emailElement.nativeElement.textContent).toContain('test@example.com'); + }); + + it('should display validation error for invalid email input', () => { + component.currentPageState = PageState.notSent; + const emailControl = component.fg.controls.email; + emailControl.setValue('invalid-email'); + emailControl.markAsTouched(); + fixture.detectChanges(); + + const errorElement = getElementRef(fixture, '.forgot-password__error-message'); + expect(errorElement.nativeElement.textContent).toContain(' Enter an email address. '); + }); + + it('should call sendResetLink with correct email when button is clicked', () => { + component.currentPageState = PageState.notSent; + spyOn(component, 'sendResetLink'); + component.fg.controls.email.setValue('test@example.com'); + fixture.detectChanges(); + + const buttonElement = fixture.debugElement.query(By.css('ion-button')); + buttonElement.triggerEventHandler('click', null); + + expect(component.sendResetLink).toHaveBeenCalledWith('test@example.com'); + }); + + it('should display resend link if email is not sent', () => { + component.isEmailSentOnce = false; + component.isLoading = false; + component.currentPageState = component.PageState.success; + component.resetEmail = 'test@example.com'; + fixture.detectChanges(); + + const resendLink = fixture.debugElement.query(By.css('.forgot-password__resend-text__resend-link')); + expect(resendLink).toBeTruthy(); + }); + + it('should hide resend link and show spinner when loading', () => { + component.isEmailSentOnce = false; + component.isLoading = true; + component.currentPageState = component.PageState.success; + fixture.detectChanges(); + + const resendLink = getElementRef(fixture, '.forgot-password__resend-text__resend-link') as DebugElement; + expect(resendLink).toBeFalsy(); + + const spinner = getElementRef(fixture, 'ion-spinner') as DebugElement; + expect(spinner).toBeTruthy(); + }); + }); + describe('sendResetLink():', () => { - it('should send reset password link, change loading and page state', () => { + beforeEach(() => { + component.currentPageState = PageState.success; + }); + + it('should send reset password link, change loading and page state to success', fakeAsync(() => { routerAuthService.sendResetPassword.and.returnValue(of({})); const email = 'jay.b@fyle.in'; component.sendResetLink(email); - expect(component.isLoading).toEqual(false); + tick(); + expect(component.isLoading).toBeFalse(); expect(component.currentPageState).toEqual(PageState.success); - }); + })); it('should send reset password link, change loading and page state', () => { routerAuthService.sendResetPassword.and.returnValue(throwError(() => new Error('Error message'))); @@ -97,28 +171,43 @@ describe('ResetPasswordPage', () => { const email = 'jay.b@fyle.in'; component.sendResetLink(email); - expect(component.isLoading).toEqual(true); + expect(component.isLoading).toBeFalse(); expect(component.handleError).toHaveBeenCalledTimes(1); }); }); - describe('handleError():', () => { - it('should navigate to disabled auth', () => { - component.handleError({ - status: 422, - message: 'Error message', - }); + describe('handleError(): ', () => { + it('should navigate to disabled page on 422 error', () => { + const error = { status: 422 }; + component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); + expect(router.navigate).toHaveBeenCalledWith(['/', 'auth', 'disabled']); }); - it('should change page state if auth not disabled', () => { - component.handleError({ - status: 400, - message: 'Error message', - }); + it('should display error message on other errors', () => { + const error = { status: 401 }; + const props = { + panelClass: ['msb-failure'], + }; + + matSnackBar.openFromComponent.and.callThrough(); - expect(component.currentPageState).toEqual(PageState.failure); + component.handleError(error); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...props, + panelClass: ['msb-failure'], + }); + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledTimes(1); }); }); + + it('onGotoSignInClick(): should navigate to sign-in page', () => { + component.onGotoSignInClick(); + expect(router.navigate).toHaveBeenCalledWith([ + '/', + 'auth', + 'sign_in', + { email: component.fg.controls.email.value }, + ]); + }); }); diff --git a/src/app/auth/reset-password/reset-password.page.ts b/src/app/auth/reset-password/reset-password.page.ts index 09d288f93d..b7555831ed 100644 --- a/src/app/auth/reset-password/reset-password.page.ts +++ b/src/app/auth/reset-password/reset-password.page.ts @@ -1,43 +1,97 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { tap } from 'rxjs/operators'; +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { finalize } from 'rxjs/operators'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { PageState } from 'src/app/core/models/page-state.enum'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; @Component({ selector: 'app-reset-password', templateUrl: './reset-password.page.html', + styleUrls: ['./reset-password.page.scss'], }) -export class ResetPasswordPage implements OnInit { +export class ResetPasswordPage { currentPageState: PageState; isLoading = false; - constructor(private routerAuthService: RouterAuthService, private router: Router) {} + fg: FormGroup; - ionViewWillEnter() { + resetEmail: string; + + isEmailSentOnce: boolean; + + PageState: typeof PageState = PageState; + + constructor( + private formBuilder: FormBuilder, + private routerAuthService: RouterAuthService, + private router: Router, + private activatedRoute: ActivatedRoute, + private matSnackBar: MatSnackBar, + private snackbarProperties: SnackbarPropertiesService + ) {} + + ionViewWillEnter(): void { this.currentPageState = PageState.notSent; + this.isEmailSentOnce = false; + const email = (this.activatedRoute.snapshot.params.email as string) || ''; + this.fg = this.formBuilder.group({ + email: [email, Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); } - ngOnInit() {} - - sendResetLink(email: string) { + sendResetLink(email: string): void { this.isLoading = true; + this.resetEmail = email; + + if (this.currentPageState === PageState.success) { + this.isEmailSentOnce = true; + } this.routerAuthService .sendResetPassword(email) - .pipe(tap(() => (this.isLoading = false))) + .pipe( + finalize(() => { + this.isLoading = false; + }) + ) .subscribe({ - next: () => (this.currentPageState = PageState.success), - error: (err) => this.handleError(err), + next: () => { + this.currentPageState = PageState.success; + if (this.isEmailSentOnce) { + const toastMessageData = { + message: ' Password recovery email sent successfully.', + }; + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('success', toastMessageData), + panelClass: ['msb-success'], + }); + } + }, + error: (err: { status: number }) => this.handleError(err), }); } - handleError(err) { + handleError(err: { status: number }): void { if (err.status === 422) { this.router.navigate(['/', 'auth', 'disabled']); } else { - this.currentPageState = PageState.failure; + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], + }); } } + + onGotoSignInClick(): void { + this.router.navigate(['/', 'auth', 'sign_in', { email: this.fg.controls.email.value as string }]); + } } diff --git a/src/app/auth/sign-in/error/error.component.html b/src/app/auth/sign-in/error/error.component.html index d7d3157e3d..e3d0d09cf6 100644 --- a/src/app/auth/sign-in/error/error.component.html +++ b/src/app/auth/sign-in/error/error.component.html @@ -1,11 +1,18 @@
-
- {{ header }} +
+ +
+ {{ header }} +
-
+
- This email address will be temporarily locked after 5 unsuccessful login attempts. Try - resetting your + This email address will be temporarily locked after 5 unsuccessful login attempts. Would you like to try + resetting your password?
@@ -31,7 +38,7 @@
Your organization has restricted Fyle access to its corporate network.
-
- +
+ Try again
diff --git a/src/app/auth/sign-in/error/error.component.scss b/src/app/auth/sign-in/error/error.component.scss index bab19843e8..496b825bc9 100644 --- a/src/app/auth/sign-in/error/error.component.scss +++ b/src/app/auth/sign-in/error/error.component.scss @@ -1,29 +1,39 @@ -$details-color: #ababab; +@import '../../../../theme/colors.scss'; .error-internal { - &--header { + &__header { font-size: 20px; padding: 16px; - font-weight: 700; - } - - &--details { - font-size: 16px; - padding: 0 16px; font-weight: 500; - color: $details-color; - } + display: flex; + flex-direction: row; + border-bottom: 1px solid $grey-lighter; + gap: 12px; - &--primary-cta { - padding: 16px; - .mat-button-base { + &__header-text { + align-items: center; + justify-content: center; width: 100%; - font-weight: 700; - min-height: 47px; + display: flex; + flex-direction: row; } } - &--redirect { + &__close-icon { + align-self: flex-start; + } + + &__primary-cta { + padding: 0 16px 16px; + } + + &__details { + font-size: 14px; + padding: 20px 16px; + color: $black; + } + + &__redirect { text-decoration: none; } } diff --git a/src/app/auth/sign-in/error/error.component.spec.ts b/src/app/auth/sign-in/error/error.component.spec.ts index 2771080c02..f92b180191 100644 --- a/src/app/auth/sign-in/error/error.component.spec.ts +++ b/src/app/auth/sign-in/error/error.component.spec.ts @@ -35,11 +35,11 @@ describe('ErrorComponent', () => { }); it('should have a default header', () => { - expect(component.header).toEqual('Account does not Exist'); + expect(component.header).toEqual('Account does not exist'); }); - it('tryAgainClicked(): should dismiss the popover on try again button click', async () => { - const tryAgainBtn = getElementBySelector(fixture, '.error-internal--primary-cta button') as HTMLButtonElement; + it('closePopover(): should dismiss the popover on try again button click', async () => { + const tryAgainBtn = getElementBySelector(fixture, '.error-internal__primary-cta ion-button') as HTMLButtonElement; click(tryAgainBtn); fixture.detectChanges(); await fixture.whenStable(); @@ -59,10 +59,10 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 401 and data is present', () => { component.error = { status: 401, data: { message: 'Invalid email or password' } }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); - const resetLink = getElementBySelector(fixture, '.error-internal--redirect'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); + const resetLink = getElementBySelector(fixture, '.error-internal__redirect'); expect(getTextContent(errorMessage)).toContain( - 'This email address will be temporarily locked after 5 unsuccessful login attempts. Try resetting your password?' + 'This email address will be temporarily locked after 5 unsuccessful login attempts. Would you like to try resetting your password?' ); expect(resetLink).toBeTruthy(); }); @@ -70,7 +70,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 400', () => { component.error = { status: 400 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'Your account is not verified. Please request a verification link, if required' ); @@ -79,7 +79,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 500', () => { component.error = { status: 500 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); const supportLink = getElementBySelector(fixture, 'a'); expect(getTextContent(errorMessage)).toContain( 'Please retry in a while. Send us a note to support@fylehq.com if the problem persists.' @@ -90,7 +90,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 433', () => { component.error = { status: 433 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'This email address is locked temporarily, as there are too many unsuccessful login attempts recently. Please retry later.' ); @@ -99,7 +99,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 401 and no data or message is present', () => { component.error = { status: 401 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'Your organization has restricted Fyle access to its corporate network.' ); diff --git a/src/app/auth/sign-in/error/error.component.ts b/src/app/auth/sign-in/error/error.component.ts index a4a2775bda..bdbd7fc782 100644 --- a/src/app/auth/sign-in/error/error.component.ts +++ b/src/app/auth/sign-in/error/error.component.ts @@ -1,27 +1,24 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { PopoverController } from '@ionic/angular'; import { Router } from '@angular/router'; -import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-error', templateUrl: './error.component.html', styleUrls: ['./error.component.scss'], }) -export class ErrorComponent implements OnInit { - @Input() header = 'Account does not Exist'; +export class ErrorComponent { + @Input() header = 'Account does not exist'; @Input() error; constructor(private popoverController: PopoverController, private router: Router) {} - ngOnInit() {} - - async tryAgainClicked() { + async closePopover(): Promise { await this.popoverController.dismiss(); } - async routeTo(route: string[]) { + async routeTo(route: string[]): Promise { this.router.navigate(route); await this.popoverController.dismiss(); } diff --git a/src/app/auth/sign-in/sign-in-page-state.enum.ts b/src/app/auth/sign-in/sign-in-page-state.enum.ts new file mode 100644 index 0000000000..5e49fc9698 --- /dev/null +++ b/src/app/auth/sign-in/sign-in-page-state.enum.ts @@ -0,0 +1,5 @@ +export enum SignInPageState { + SELECT_SIGN_IN_METHOD = 'SELECT_SIGN_IN_METHOD', // Google Sign In and normal sign in redirection from here + ENTER_EMAIL = 'ENTER_EMAIL', // user can enter email and proceed to next step, SSO flow begins after this step + ENTER_PASSWORD = 'ENTER_PASSWORD', // user can enter their password for login here +} diff --git a/src/app/auth/sign-in/sign-in.page.html b/src/app/auth/sign-in/sign-in.page.html index d394589c3b..eff20a80c3 100644 --- a/src/app/auth/sign-in/sign-in.page.html +++ b/src/app/auth/sign-in/sign-in.page.html @@ -1,125 +1,163 @@ -