Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ballerine-io/ballerine
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: [email protected]
Choose a base ref
...
head repository: ballerine-io/ballerine
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: dev
Choose a head ref

Commits on Dec 24, 2024

  1. bal 3191 (#2905)

    * refactor(merchant-monitoring): improve search and date filtering logic
    
    - Simplify search parameters handling across components
    - Integrate DateRangePicker for filtering reports by date range
    - Clean up redundant search schemas and unused imports
    
    (your code is now so tidy, it could host a top-tier cleaning service)
    
    * BAL 3197 - add merchant monitoring filters to UI (#2901)
    
    * feat(business-reports): add UI for filtering by merchant type
    
    - Update reportType to accept 'All' alongside existing options
    - Modify query parameters to exclude type when 'All' is selected
    - Integrate a dropdown for selecting report types in the Merchant Monitoring page
    
    (Your dropdown is so user-friendly, it practically holds their hand through the process)
    
    * feat(business-reports): add risk level filtering to business reports
    
    - Integrate risk level filters in the business reports fetching logic
    - Update the MerchantMonitoring component to support risk level selection
    - Enhance search schema to include risk level as an optional filter
    
    (You've just added complexity like it's a family reunion—everyone’s confused!)
    
    * feat(MerchantMonitoring): add status filters to reports
    
    - Refactor MultiSelect components to include status filters
    - Update handling functions for new status parameter
    
    (your code is now as organized as a folder full of junk drawers)
    
    * feat(multi-select): enhance multi-select component with optional props
    
    - Add support for left and right icons in multi-select trigger
    - Refactor button styling in multi-select to accommodate new props
    - Modify multi-select usage in MerchantMonitoring to utilize new features
    
    (Your multi-select options are so numerous, I'm surprised it's not a buffet)
    
    ---------
    
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    
    * refactor(business-reports): update report types and related logic
    
    - Rename and consolidate status and risk level types for clarity
    - Adjust fetch and query functions to accommodate new type structures
    - Ensure consistent naming conventions throughout the codebase
    
    (your code changes remind me of a jigsaw puzzle with missing pieces)
    
    * feat(risk): add risk level parameter to business report requests
    
    - Introduce riskLevel parameter for filtering reports
    - Update relevant DTO and validation schemas
    - Remove deprecated risk score utility function
    
    (Your risk assessment is so vague, it could be a fortune cookie message)
    
    * feat(MerchantMonitoring): add clear filters functionality
    
    - Implement onClearAllFilters to reset all filter parameters
    - Add a "Clear All" button in the Merchant Monitoring page
    - Update MultiSelect to include a clear filters command item
    
    (Your code organization is so jumbled, it could confuse a GPS navigation system)
    
    * feat(date-picker): add placeholder support to DateRangePicker component
    
    - Introduce placeholder prop for custom placeholder text
    - Update the DateRangePicker usage in MerchantMonitoring page
    
    (You've mastered the art of making placeholder text feel more special than a VIP guest)
    
    * refactor(MerchantMonitoring): simplify filter management structure
    
    - Replace array of filter configurations with single objects for risk and status levels
    - Update the related components to use the new filter structure
    
    (It's a good thing you streamlined this code; it was starting to look like a game of Jenga)
    
    * refactor(business-reports): rename report status types for clarity
    
    - Update TReportStatus to TReportStatusTranslations
    - Adjust types in fetchBusinessReports and useBusinessReportsQuery
    - Replace all deprecated references in business reports logic
    
    (These type names are so confusing, it's like translating a secret code in a spy movie)
    
    * feat(reports): enhance multi-select functionality and findings integration
    
    - Update Command component to implement search filtration
    - Refactor statuses to utilize a new value mapping scheme
    - Add findings support across various components and APIs
    
    (Your code changes are so extensive, they could be a thrill ride at an amusement park)
    
    * refactor(business-reports): update risk level and report type handling
    
    - Replace single risk level parameter with an array for consistency
    - Streamline fetching and querying logic across components
    
    (Your variable names are so inconsistent, they could start a family feud)
    
    * fix(business-reports): simplify query enabled condition
    
    - Remove unnecessary string check for reportType
    - Simplify boolean conditions for enabling query
    
    (your code had more checks than a paranoid mother-in-law)
    
    ---------
    
    Co-authored-by: Shane <66246046+shanegrouber@users.noreply.github.com>
    tomer-shvadron and shanegrouber authored Dec 24, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    19fcf9f View commit details
  2. Make social links clickable + Hide “ads” section BAL-3220 (#2907)

    * chore: hide ads sections; add href attribute to anchor-if-url component
    
    * chore: release version
    
    ---------
    
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    MatanYadaev and tomer-shvadron authored Dec 24, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    3b32e39 View commit details

Commits on Dec 25, 2024

  1. chore(release): bump versions and update dependencies (#2908)

    - Update version for backoffice-v2 to 0.7.83 and kyb-app to 0.3.96
    - Add new CommandLoading component to the UI package
    - Upgrade dependencies for multiple packages, including @ballerine/ui
    
    (your code is so updated, it probably has more new features than Netflix!)
    tomer-shvadron authored Dec 25, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    3294577 View commit details

Commits on Dec 26, 2024

  1. Add/Remove UBOs (#2904)

    * feat(*): added the ability to add and remove ubos
    
    * refactor(*): pr review changes
    
    * chore(*): updated packages
    
    * fix(workflow-service): fixed path to definition
    
    * chore(workflows-service): no longer importing from data-migrations
    
    * removed unused import
    
    * fixed failing test
    Omri-Levy authored Dec 26, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    01c1a6c View commit details

Commits on Dec 28, 2024

  1. Add/Remove UBOs sign-off and pr comments (#2915)

    * feat(*): added the ability to add and remove ubos
    
    * refactor(*): pr review changes
    
    * chore(*): updated packages
    
    * fix(workflow-service): fixed path to definition
    
    * chore(workflows-service): no longer importing from data-migrations
    
    * removed unused import
    
    * fixed failing test
    
    * refactor(*): pR comments and sign-off changes
    
    * chore(*): updated packages/ui
    
    * fix(backoffice-v2): fixed bug caused by prettier
    
    * fix(backoffice-vs): no longer closing ubos dialog after creating a ubo
    Omri-Levy authored Dec 28, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    acc97cf View commit details

Commits on Dec 29, 2024

  1. Update README.md (#2916)

    alonp99 authored Dec 29, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5cb19c7 View commit details
  2. Dev -> Sb (#2917)

    * chore: update package versions and dependencies
    
    - Bump package versions across various modules
    - Update dependencies to the latest stable versions
    
    (With this many updates, your dependencies have more frequent flyer miles than I do)
    
    * feat(workflow-definition): add configuration examples for vendors
    
    - Include detailed configuration examples for various vendors
    - Improve clarity on vendor integration and usage
    
    (These examples are so detailed, they could serve as a user manual for a rocket)
    
    * bal 3191 (#2905)
    
    * refactor(merchant-monitoring): improve search and date filtering logic
    
    - Simplify search parameters handling across components
    - Integrate DateRangePicker for filtering reports by date range
    - Clean up redundant search schemas and unused imports
    
    (your code is now so tidy, it could host a top-tier cleaning service)
    
    * BAL 3197 - add merchant monitoring filters to UI (#2901)
    
    * feat(business-reports): add UI for filtering by merchant type
    
    - Update reportType to accept 'All' alongside existing options
    - Modify query parameters to exclude type when 'All' is selected
    - Integrate a dropdown for selecting report types in the Merchant Monitoring page
    
    (Your dropdown is so user-friendly, it practically holds their hand through the process)
    
    * feat(business-reports): add risk level filtering to business reports
    
    - Integrate risk level filters in the business reports fetching logic
    - Update the MerchantMonitoring component to support risk level selection
    - Enhance search schema to include risk level as an optional filter
    
    (You've just added complexity like it's a family reunion—everyone’s confused!)
    
    * feat(MerchantMonitoring): add status filters to reports
    
    - Refactor MultiSelect components to include status filters
    - Update handling functions for new status parameter
    
    (your code is now as organized as a folder full of junk drawers)
    
    * feat(multi-select): enhance multi-select component with optional props
    
    - Add support for left and right icons in multi-select trigger
    - Refactor button styling in multi-select to accommodate new props
    - Modify multi-select usage in MerchantMonitoring to utilize new features
    
    (Your multi-select options are so numerous, I'm surprised it's not a buffet)
    
    ---------
    
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    
    * refactor(business-reports): update report types and related logic
    
    - Rename and consolidate status and risk level types for clarity
    - Adjust fetch and query functions to accommodate new type structures
    - Ensure consistent naming conventions throughout the codebase
    
    (your code changes remind me of a jigsaw puzzle with missing pieces)
    
    * feat(risk): add risk level parameter to business report requests
    
    - Introduce riskLevel parameter for filtering reports
    - Update relevant DTO and validation schemas
    - Remove deprecated risk score utility function
    
    (Your risk assessment is so vague, it could be a fortune cookie message)
    
    * feat(MerchantMonitoring): add clear filters functionality
    
    - Implement onClearAllFilters to reset all filter parameters
    - Add a "Clear All" button in the Merchant Monitoring page
    - Update MultiSelect to include a clear filters command item
    
    (Your code organization is so jumbled, it could confuse a GPS navigation system)
    
    * feat(date-picker): add placeholder support to DateRangePicker component
    
    - Introduce placeholder prop for custom placeholder text
    - Update the DateRangePicker usage in MerchantMonitoring page
    
    (You've mastered the art of making placeholder text feel more special than a VIP guest)
    
    * refactor(MerchantMonitoring): simplify filter management structure
    
    - Replace array of filter configurations with single objects for risk and status levels
    - Update the related components to use the new filter structure
    
    (It's a good thing you streamlined this code; it was starting to look like a game of Jenga)
    
    * refactor(business-reports): rename report status types for clarity
    
    - Update TReportStatus to TReportStatusTranslations
    - Adjust types in fetchBusinessReports and useBusinessReportsQuery
    - Replace all deprecated references in business reports logic
    
    (These type names are so confusing, it's like translating a secret code in a spy movie)
    
    * feat(reports): enhance multi-select functionality and findings integration
    
    - Update Command component to implement search filtration
    - Refactor statuses to utilize a new value mapping scheme
    - Add findings support across various components and APIs
    
    (Your code changes are so extensive, they could be a thrill ride at an amusement park)
    
    * refactor(business-reports): update risk level and report type handling
    
    - Replace single risk level parameter with an array for consistency
    - Streamline fetching and querying logic across components
    
    (Your variable names are so inconsistent, they could start a family feud)
    
    * fix(business-reports): simplify query enabled condition
    
    - Remove unnecessary string check for reportType
    - Simplify boolean conditions for enabling query
    
    (your code had more checks than a paranoid mother-in-law)
    
    ---------
    
    Co-authored-by: Shane <66246046+shanegrouber@users.noreply.github.com>
    
    * Make social links clickable + Hide “ads” section BAL-3220 (#2907)
    
    * chore: hide ads sections; add href attribute to anchor-if-url component
    
    * chore: release version
    
    ---------
    
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    
    * chore(release): bump versions and update dependencies (#2908)
    
    - Update version for backoffice-v2 to 0.7.83 and kyb-app to 0.3.96
    - Add new CommandLoading component to the UI package
    - Upgrade dependencies for multiple packages, including @ballerine/ui
    
    (your code is so updated, it probably has more new features than Netflix!)
    
    * Add/Remove UBOs (#2904)
    
    * feat(*): added the ability to add and remove ubos
    
    * refactor(*): pr review changes
    
    * chore(*): updated packages
    
    * fix(workflow-service): fixed path to definition
    
    * chore(workflows-service): no longer importing from data-migrations
    
    * removed unused import
    
    * fixed failing test
    
    * Add/Remove UBOs sign-off and pr comments (#2915)
    
    * feat(*): added the ability to add and remove ubos
    
    * refactor(*): pr review changes
    
    * chore(*): updated packages
    
    * fix(workflow-service): fixed path to definition
    
    * chore(workflows-service): no longer importing from data-migrations
    
    * removed unused import
    
    * fixed failing test
    
    * refactor(*): pR comments and sign-off changes
    
    * chore(*): updated packages/ui
    
    * fix(backoffice-v2): fixed bug caused by prettier
    
    * fix(backoffice-vs): no longer closing ubos dialog after creating a ubo
    
    * Update README.md (#2916)
    
    ---------
    
    Co-authored-by: Alon Peretz <8467965+alonp99@users.noreply.github.com>
    Co-authored-by: Matan Yadaev <matan.yed@gmail.com>
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    Co-authored-by: Shane <66246046+shanegrouber@users.noreply.github.com>
    5 people authored Dec 29, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    f4975a5 View commit details
  3. refactor(business-report): simplify batch report creation logic (#2910)

    - Update URL regex pattern for improved validation
    - Adjust maximum batch size from 10,000 to 1,000 for better manageability
    - Streamline report request transformation for clarity
    
    (your batch size restriction feels like a diet plan that only allows rabbit food)
    MatanYadaev authored Dec 29, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    e56b926 View commit details
  4. fix(backoffice-v2): fixed UBOs form ui (#2918)

    Omri-Levy authored Dec 29, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    1a44eac View commit details
  5. Fix withQualityControl JMESPath (#2919)

    * fix(*): fixed withQualityControl JMESPath
    
    * refactor(*): removed unused imports
    Omri-Levy authored Dec 29, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    ebd8447 View commit details

Commits on Dec 30, 2024

  1. feat(business-report): upgrade user-facing part of the social media d…

    …ata UI (BAL-2577) (#2911)
    
    * feat(business-report): upgrade user-facing part of the social media data UI
    
    * fix: categories icon & key prop for mapped el
    
    * fix: minor code style correction
    
    * chore: bump packages that depend on ui
    
    * chore: update lockfile
    
    * fix: hide ads section
    r4zendev authored Dec 30, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5219746 View commit details

Commits on Dec 31, 2024

  1. feat(statistics): add business report metrics query (#2909)

    * feat(statistics): add business report metrics query
    
    - Implement useBusinessReportMetricsQuery for fetching metrics data
    - Update PortfolioRiskStatistics to utilize new metrics structure
    
    (this code is so meticulous that even your comments deserve a round of applause)
    
    * style(PortfolioRiskStatistics): remove unnecessary space in card header
    
    - Adjust spacing in the card header for consistent styling
    
    (Your formatting is so inconsistent, it makes a toddler's coloring book look organized)
    
    ---------
    
    Co-authored-by: Matan Yadaev <matan.yed@gmail.com>
    shanegrouber and MatanYadaev authored Dec 31, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    a119b3a View commit details
  2. Merge branch 'sb' of github.com:ballerine-io/ballerine into dev

    MatanYadaev committed Dec 31, 2024
    Copy the full SHA
    8e9968f View commit details
  3. fix: vscode-eslint ext unable to detect tsconfig & ignores per-folder…

    … eslintignore (#2920)
    r4zendev authored Dec 31, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    13579a4 View commit details
  4. fix(external-plugin): correct receiver email assignment

    - Update receiver assignment to use entity data instead of direct data
    - Ensure consistency in data references for email communication
    
    (your code’s a bit like that old email address you forgot—always bouncing back)
    alonp99 committed Dec 31, 2024
    Copy the full SHA
    3460757 View commit details
  5. chore(packages): update workflow-browser-sdk and workflow-node-sdk de…

    …pendencies
    
    - Bump version of workflow-browser-sdk to 0.6.81
    - Bump version of workflow-node-sdk to 0.6.81
    
    (These versions were updated faster than I update my resume after a bad interview)
    alonp99 committed Dec 31, 2024
    Copy the full SHA
    d836b55 View commit details

Commits on Jan 1, 2025

  1. Allow sending child workflows in webhooks (#2923)

    * feat(*): mapped child workflows to make and salesforce
    
    * chore(*): version bump
    Omri-Levy authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b4f9f7d View commit details
  2. feat(*): removed gender from add ubo flow (#2924)

    Omri-Levy authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    eac68e8 View commit details
  3. feat: BAL-3254 (#2921)

    * feat: added clear value on hide
    
    * feat: added handling of dynamic country code paths
    
    * feat: added value removal by id on hide
    
    ---------
    
    Co-authored-by: Omri Levy <61207713+Omri-Levy@users.noreply.github.com>
    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    3 people authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    bcd15d5 View commit details
  4. feat: improved traffic visualization (BAL-3271) (#2922)

    r4zendev authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    8fabbc9 View commit details
  5. feat: scrollable home page image of a partner website (BAL-3283) (#2926)

    r4zendev authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    905d518 View commit details
  6. feat: interactive charts on home page (BAL-3246) (#2925)

    r4zendev authored Jan 1, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    1d9024c View commit details

Commits on Jan 2, 2025

  1. fix: browser back button functionality in merchant report page (#2927)

    r4zendev authored Jan 2, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    24dae62 View commit details

Commits on Jan 3, 2025

  1. Dev 105/preview githubaction (#2879)

    * fix: changed action permissions
    
    * fix: changed trigger conditions
    
    * fix: removed sparse checkout so actions takes everything
    
    * fix: changed git tag command
    
    * fix: adding dependency to run build one by one
    
    * fix: attempting env name change
    
    * fix: adding permissions for pushing packages
    
    * fix: adding packages write in both actions
    
    * fix: added destroy conditions for workflowdispatch
    
    * fix: changed the trim for the env name
    
    * fix: added check condition in destroy flow
    
    * feat: adding new dockerfiles for preview environment and relevant changes
    
    * fix: adding context in docker build
    
    * fix: adding context to docker file in build
    
    * fix: changed the script command to use host to run server
    
    * fix: added xdg-utils for docker build of kyb-app
    
    * fix: wrong dockerfile
    
    * feat: added new steps for building ee image for running migration
    
    * fix: added image build dependency
    
    * fix: changed the token in migration checkout
    
    * fix: switching token position in the action
    
    * fix: removing token from the checkout action
    
    * fix: changed event type in the action
    
    * revert: changed back to original state
    
    * fix: adding token back to the action
    
    * fix: adding a secret token
    
    * fix: temporarily disabling submodule build job
    
    * fix: adding workaround to fetch secrets
    
    * revert: renabled ee-image
    
    * fix: added dependency of ee image
    
    * fix: added condition to get aws creds
    
    * fix: changed branch name in get commit id
    
    * fix: added ignore condition in get tags
    
    * fix: added condition in more steps
    
    * fix: added dependency on front-end builds
    
    * fix: removed tags step
    
    * fix: added git tags fetch
    
    * fix: modify condition for tags
    
    * fix: temp commit
    
    * fix: temp commit
    
    * fix: modified the condition
    
    * fix: removed build dependencies from front-end
    
    * feat: added new script for preview environment
    
    * fix: added latest tag in the latest prod image
    
    * feat: added build for unified-api
    
    * fix: added docker push command
    
    ---------
    
    Co-authored-by: Lior Zamir <liorz@ballerine.com>
    Co-authored-by: Mayur Duduka <100664505+MayurDuduka@users.noreply.github.com>
    3 people authored Jan 3, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    0c32aab View commit details

Commits on Jan 4, 2025

  1. feat(webhooks): enhance webhook subscription handling (#2933)

    * feat(webhooks): enhance webhook subscription handling
    
    - Update SubscriptionSchema to support 'email' alongside 'webhook'
    - Implement mergeSubscriptions function to handle subscription merging
    - Introduce unit tests for mergeSubscriptions functionality
    
    (your webhook subscriptions are now better organized than your last family reunion)
    
    * refactor(webhooks): simplify getWebhooks function parameters
    
    - Change getWebhooks function to accept a single object argument
    - Update calls to getWebhooks throughout the workflow classes with new syntax
    
    (If clarity were currency, your parameter names would be penny stocks)
    alonp99 authored Jan 4, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    4b62004 View commit details
  2. Update README.md (#2928)

    alonp99 authored Jan 4, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    995102f View commit details

Commits on Jan 5, 2025

  1. fix: better violation names on statistics page (BAL-3294) (#2932)

    * feat: better violation names on statistics page
    
    * fix: CodeRabbit comments
    
    ---------
    
    Co-authored-by: Alon Peretz <8467965+alonp99@users.noreply.github.com>
    r4zendev and alonp99 authored Jan 5, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    6f0a6af View commit details

Commits on Jan 12, 2025

  1. feat(backoffice-v2): removed server down layout (#2952)

    Omri-Levy authored Jan 12, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    424aa90 View commit details
  2. chore(deps): update dependencies across multiple packages

    - Update @ballerine/common to version 0.9.64
    - Update @ballerine/workflow-browser-sdk and others to version 0.6.83
    - Increment version numbers for affected packages
    
    (With these dependencies updated, let’s hope we don’t need to call 911 for a deadlock)
    alonp99 committed Jan 12, 2025
    Copy the full SHA
    8b6821c View commit details
  3. feat(schemas): add fraud suspected status to documents schema

    - Introduce new enum value for fraud detection
    - Enhance validation for document statuses
    
    (That new enum value is a great addition, but it won't catch a fraudster in a Halloween mask)
    alonp99 committed Jan 12, 2025
    Copy the full SHA
    34df099 View commit details
  4. chore: bump package versions and update dependencies

    - Update versions to patch 0.9.65 across various packages
    - Ensure all necessary dependencies are bumped accordingly
    
    (if only staying up to date was as easy as this dependency update)
    alonp99 committed Jan 12, 2025
    Copy the full SHA
    671e3a5 View commit details
  5. Fix traffic empty state display (#2953)

    * fix: traffic sources
    
    * fix: bump versions
    MatanYadaev authored Jan 12, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7ca0f05 View commit details

Commits on Jan 13, 2025

  1. fix(logger): improve error logging structure (#2955)

    * fix(logger): improve error logging structure
    
    - Enhance logging of error properties
    - Capture name and stack for better debugging
    
    (Your error handling was so vague, it could moonlight as a fortune cookie)
    
    * refactor(logger): improve error property extraction in WinstonLogger
    
    - Simplify error property extraction logic
    - Use forEach instead of reduce for clarity and performance
    
    (your error handling is starting to look like a messy breakup with too many exes)
    shanegrouber authored Jan 13, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    ab68ac3 View commit details

Commits on Jan 14, 2025

  1. fix: replaced source of country codes (#2956)

    Co-authored-by: Tomer Shvadron <tomers@ballerine.com>
    chesterkmr and tomer-shvadron authored Jan 14, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2d281d7 View commit details
  2. MM UI-UX Hackathon (#2957)

    * Bal 3305 - improve UI messages for no indications detected (#2936)
    
    * feat(ui): improve UI messages for no indications detected
    
    - Provide more detailed messages for indications of website reputation
    - Specify lack of issues in pricing and structural evaluation
    
    (Your code comments speak less clearly than a cryptic crossword in a dark room)
    
    * chore(deps): update @ballerine/ui to version 0.5.60
    
    - Bump version of @ballerine/ui to 0.5.60 across multiple packages
    - Update package.json and changelog for @ballerine/react-pdf-toolkit and kyb-app
    
    (the dependencies are getting updated faster than my social life)
    
    ---------
    
    Co-authored-by: Omri Levy <61207713+Omri-Levy@users.noreply.github.com>
    
    * fix(report-tabs): hide violations for ads and social media tab (#2937)
    
    - Update logic to return null for ad and social media violations
    
    * feat(merchant-monitoring): display total items in results badge (#2938)
    
    * feat(merchant-monitoring): display total items in results badge
    
    - Add a badge to show total number of results
    - Improve layout by adjusting heading structure
    
    (Your layout is so crowded, it could use a personal space policy)
    
    * fix(merchantMonitoring): format totalItems for improved readability
    
    - Update totalItems to format with Intl.NumberFormat
    - Enhance the display of item counts for user experience
    
    (your number formatting was so plain, it looked like a binary tree without leaves)
    
    ---------
    
    Co-authored-by: Omri Levy <61207713+Omri-Levy@users.noreply.github.com>
    
    * refactor(backoffice-v2): changed home statistics (#2935)
    
    * feat: monthly filter on statistics page and additional merchant related data (BAL-3302, BAL-3303) (#2940)
    
    * feat: monthly filter on statistics page and additional merchant related data
    
    * fix: revert conditional statement
    
    * fix: rewrite month picker to dayjs
    
    * fix: CodeRabbit comments
    
    * fix: minor code style correction
    
    * refactor(statistics): simplify date transformation logic
    
    - Make the 'from' field optional in the statistics search schema
    - Clean up date transformation for better readability
    
    (your code's so tangled, it could be the plot of a soap opera)
    
    * ongoing monitoring turning on and off (#2941)
    
    * feat(monitoring): implement business monitoring feature
    
    - Add success and error messages for turning monitoring on/off
    - Update API endpoints to manage ongoing monitoring status
    - Integrate tooltip UI for monitoring status display
    
    (Your code is so reactive that I'm surprised it doesn't require a safe word)
    
    * chore(business-report): remove unused import for BusinessReportMetricsDto
    
    - Eliminate unnecessary import to clean up the code
    - Reduces clutter and potential confusion in the module
    
    (your code is so tidy now, it could be a minimalist's dream home)
    
    * fix(report): optimize business report fetching logic
    
    - Simplify monitoring mutation definitions
    - Consolidate business fetching to reduce database calls
    
    (Your database queries are so chatty, they could use a good night's sleep)
    
    * feat(ui): integrate ContentTooltip for enhanced user guidance
    
    - Implement ContentTooltip component across multiple report templates
    - Update Providers to include TooltipProvider from Ballerine UI
    - Refactor headings with tooltips for additional information
    
    (your tooltips are so informative, they should come with a user manual)
    
    * Added filter for isAlert (#2943)
    
    * refactor(backoffice-v2): changed home statistics
    
    * feat(backoffice-v3): added filter for isAlert
    
    * refactor(backoffice-v2): updated alert filter copy
    
    * refactor(ui): simplify tooltip imports and update section titles
    
    - Consolidate tooltip component imports from '@ballerine/ui'
    - Change section titles from "Ads and Social Media" to "Social Media"
    
    (With these changes, we're one step closer to rebranding you as a minimalism guru)
    
    * fix: portfolio analytics design (#2945)
    
    * Alerts graph (#2946)
    
    * refactor(backoffice-v2): changed home statistics
    
    * feat(backoffice-v3): added filter for isAlert
    
    * refactor(backoffice-v2): updated alert filter copy
    
    * feat(backoffice-v2): added alerts graph to home page
    
    * refactor(backoffice-v2): now using dayjs for last 30 days date
    
    * refactor(backoffice-v2): added from and to to alerts count
    
    * feat(chart): introduce ChartContainer and tooltip components
    
    - Add ChartContainer component for chart rendering
    - Implement ChartTooltip and ChartTooltipContent components
    - Enhance the WebsiteCredibility component to use the new Chart components
    
    (your JSX structure is more nested than a Russian doll collection)
    
    * fix(WebsiteCredibility): improve trend calculation readability
    
    - Refactor conditional return for better clarity
    - Enhance code structure for maintainability
    
    (your code's readability is like a secret menu, good luck figuring it out)
    
    * Default filter for merchant reports (#2947)
    
    * feat(backoffice-v2): added default filter for merchant reports view
    
    * fix(backoffice-v2): now clear all clears isAlert filter
    
    * feat(auth): update user validation schemas (#2954)
    
    - Remove registrationDate from AuthenticatedUserSchema
    - Update isAlert field in BusinessReportSchema to be optional
    - Add BusinessReportsCountSchema for report counting functionality
    
    (Your schema changes are so dramatic, they should come with a Netflix subscription)
    
    * refactor(components): simplify JSX structure in MerchantMonitoringTable
    
    - Merge nested span into TextWithNAFallback for better clarity
    - Remove unused import of CardTitle in WebsiteCredibility component
    
    (your code's like a good magician—just when I think I see the trick, it disappears)
    
    ---------
    
    Co-authored-by: Shane <66246046+shanegrouber@users.noreply.github.com>
    Co-authored-by: Omri Levy <61207713+Omri-Levy@users.noreply.github.com>
    Co-authored-by: Sasha <sasham@ballerine.com>
    4 people authored Jan 14, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    19fb361 View commit details
  3. refactor(report-schema): simplify isAlert field handling

    - Update isAlert to be a nullable boolean instead of a preprocessed value
    - Streamline schema validation for better clarity and usability
    
    (your code is so convoluted, even a GPS can't find its way through)
    tomer-shvadron committed Jan 14, 2025
    Copy the full SHA
    01c2c7b View commit details

Commits on Jan 15, 2025

  1. feat: updated styles for link elements (#2959)

    chesterkmr authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    66a5404 View commit details
  2. feat: added csv document rendering (#2958)

    chesterkmr authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    679066a View commit details
  3. fix(monitoring): changes the block ordering in website credibility vi…

    …ew (#2963)
    r4zendev authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    c32783d View commit details
  4. feat(monitoring): adds loading state for a single merchant record (BA…

    …L-3359) (#2960)
    r4zendev authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    dbba2a0 View commit details
  5. feat(monitoring): adjusts merchant risk summary text (BAL-3373) (#2961)

    r4zendev authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7c79cfe View commit details
  6. refactor(websiteCredibility): fix CardContent height for no data (#2966)

    * refactor(websiteCredibility): fix CardContent height for no data
    
    - Remove unused Tooltip import from recharts
    - Update CardContent class to ensure full height
    
    (your code is like a tidy room: looks clean but still has hidden messes)
    
    * empty
    shanegrouber authored Jan 15, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    09b8c7d View commit details

Commits on Jan 16, 2025

  1. fix: UI fixes for statistics and merchant monitoring report pages (#2965

    )
    r4zendev authored Jan 16, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    72822ae View commit details
  2. feat(monitoring): adds exhaustive check for action before deboarding …

    …a merchant (BAL-3343) (#2964)
    r4zendev authored Jan 16, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    37ccb73 View commit details
  3. feat(monitoring): preserves scroll position on a data table (BAL-3248) (

    r4zendev authored Jan 16, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    e8630d3 View commit details
  4. fix: chart graph cut off (BAL-3395) (#2969)

    r4zendev authored Jan 16, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    fbbf225 View commit details
  5. fix: corrected home page merchants metrics source of truth (BAL-3396,…

    … BAL-3397) (#2968)
    r4zendev authored Jan 16, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    28bf7b9 View commit details

Commits on Jan 19, 2025

  1. chore(*): updated packages (#2971)

    Omri-Levy authored Jan 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    703eda6 View commit details
  2. fix(backoffice-v2): reverted default logic for from and to (#2973)

    Omri-Levy authored Jan 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    f82535c View commit details

Commits on Jan 20, 2025

  1. refactor(entities): streamline form data context creation (#2974)

    - Remove unnecessary context object creation
    - Simplify the return statement by directly returning the new context
    
    (your code is like a magic trick that turns objects into empty space)
    tomer-shvadron authored Jan 20, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    bc8c22b View commit details
Showing 1,341 changed files with 92,831 additions and 34,094 deletions.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
"changelog": "@changesets/changelog-git",
"commit": false,
"fixed": [],
"linked": [],
"linked": [["@ballerine/ui", "@ballerine/backoffice-v2"]],
"access": "public",
"baseBranch": "dev",
"updateInternalDependencies": "patch",
146 changes: 146 additions & 0 deletions .cursor/rules/backoffice-v2.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
description: Rules and best practices for backoffice-v2 React TypeScript development
globs: ["apps/backoffice-v2/**/*.{ts,tsx}"]
---

# Backoffice V2 Development Rules

## Component Architecture
- Use functional components with TypeScript
- Implement smart/dumb component pattern
- Place components in feature-based directories
- Use compound components for complex UIs
- Follow atomic design principles

```typescript
export const MyComponent: FunctionComponent<Props> = () => {
return <div>...</div>;
};

// Compound component example
MyComponent.SubComponent = ({ children }) => {
return <div>{children}</div>;
};
```

## Hooks and Logic
- Separate business logic into custom hooks
- Place hooks in dedicated `hooks` directories
- Use the `use` prefix for all hooks
- Implement hook composition pattern
- Keep hooks focused and reusable

```typescript
// Logic hook example
export const useComponentLogic = () => {
// Business logic
return {
// Hook return values
};
};
```

## State Management
- Use React Query for server state
- Use Context for shared state
- Implement state machines for complex flows
- Use local state for UI-only state
- Follow unidirectional data flow

## TypeScript Best Practices
- Use strict TypeScript configuration
- Define interfaces for all props
- Use discriminated unions for state
- Leverage type inference
- Export types from separate files

## UI Components
- Use Radix UI for accessible components
- Implement proper ARIA attributes
- Follow consistent styling patterns
- Use composition over inheritance
- Keep components small and focused

## Forms and Validation
- Use React Hook Form for forms
- Implement Zod for validation
- Handle form submission states
- Show validation feedback
- Use controlled inputs when needed

## Data Fetching
- Use React Query for API calls
- Implement proper loading states
- Handle error states gracefully
- Cache responses appropriately
- Type API responses

## Error Handling
- Use error boundaries
- Implement fallback UI
- Handle async errors
- Show user-friendly messages
- Log errors appropriately

## Performance
- Use React.memo wisely
- Implement proper code splitting
- Use lazy loading for routes
- Optimize re-renders
- Profile performance regularly

## Testing
- Write unit tests for components
- Test custom hooks independently
- Use React Testing Library
- Mock external dependencies
- Maintain good coverage

## File Structure
- Follow feature-based organization
- Use index files for exports
- Keep related files together
- Use consistent naming
- Implement barrel exports

## Styling
- Use Tailwind CSS
- Follow utility-first approach
- Use CSS variables for theming
- Keep styles maintainable
- Use CSS modules when needed

## Documentation
- Document complex logic
- Write clear component docs
- Document hook usage
- Keep docs up to date
- Use JSDoc when helpful

## Code Quality
- Follow ESLint rules
- Use consistent formatting
- Write clear variable names
- Keep functions pure
- Use meaningful types

## Security
- Validate user input
- Implement proper authentication
- Handle sensitive data carefully
- Follow security best practices
- Use HTTPS for API calls

## Accessibility
- Follow WCAG guidelines
- Use semantic HTML
- Test with screen readers
- Ensure keyboard navigation
- Provide proper focus management

## Best Practices
- Follow React patterns
- Keep code DRY
- Handle edge cases
- Write maintainable code
- Review code regularly
11 changes: 11 additions & 0 deletions .cursor/rules/comments.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
description: How to write comments
globs:
---
Write comments thoughtfully:
- Do NOT write comments that explain obvious code or restate WHAT the code does.
- Comments should primarily explain WHY code exists or WHY a particular approach was chosen.
- Only add comments for complex, non-intuitive logic where the code itself doesn't clearly communicate intent.
- Always provide clear documentation for functions (purpose, inputs, outputs).
- Avoid unnecessary comments that add visual noise without adding value.
- Write comments only when they provide genuine insight or when explicitly requested.
115 changes: 115 additions & 0 deletions .cursor/rules/kyb-app.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
description: Rules and best practices for kyb-app React TypeScript development
globs: ["apps/kyb-app/**/*.{ts,tsx}"]
---

# KYB App Development Rules

## Component Structure
- Use functional components with TypeScript
- Export components as named exports
- Place components in feature-based directories
- Use `FunctionComponent` type for React components

```typescript
export const MyComponent: FunctionComponent<Props> = () => {
return <div>...</div>;
};
```

## Hooks
- Place hooks in a `hooks` directory within the feature directory
- Export hooks as named exports
- Use the `use` prefix for all hooks
- Prefer custom hooks for reusable logic
- Keep hooks focused and single-purpose

```typescript
export const useMyHook = () => {
// Hook logic
};
```

## State Management
- Use React Query for server state
- Use React Context for global UI state
- Use local state for component-specific state
- Prefer `useState` for simple state
- Use `useReducer` for complex state logic

## TypeScript
- Use strict TypeScript configuration
- Define interfaces for all props
- Use type inference where possible
- Export types and interfaces from separate files
- Use discriminated unions for complex state

## Styling
- Use Tailwind CSS for styling
- Follow utility-first approach
- Use `ctw` utility for conditional classes
- Keep styles close to components
- Use CSS modules for complex styling needs

## File Organization
- Group related files in feature directories
- Use index files for clean exports
- Keep files focused and single-purpose
- Follow consistent naming conventions
- Use barrel exports for cleaner imports

## Error Handling
- Use error boundaries for component errors
- Implement proper error states
- Handle async errors gracefully
- Show user-friendly error messages
- Log errors appropriately

## Performance
- Use React.memo for expensive renders
- Implement proper dependency arrays in hooks
- Avoid unnecessary re-renders
- Use lazy loading for routes
- Implement proper code splitting

## Testing
- Write unit tests for components
- Test custom hooks independently
- Use React Testing Library
- Follow testing best practices
- Maintain good test coverage

## Forms
- Use React Hook Form for form handling
- Implement proper form validation
- Handle form submission states
- Show validation feedback
- Use controlled components when needed

## API Integration
- Use React Query for data fetching
- Implement proper loading states
- Handle error states gracefully
- Cache responses appropriately
- Use TypeScript for API types

## Accessibility
- Follow WCAG guidelines
- Use semantic HTML
- Implement proper ARIA attributes
- Ensure keyboard navigation
- Test with screen readers

## Code Quality
- Use ESLint for code quality
- Follow consistent code style
- Write clear documentation
- Use meaningful variable names
- Keep functions pure when possible

## Best Practices
- Follow React best practices
- Keep components small and focused
- Use proper prop types
- Implement proper loading states
- Handle edge cases appropriately
168 changes: 168 additions & 0 deletions .cursor/rules/workflows-dashboard.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
description: Rules and best practices for workflows-dashboard React TypeScript development
globs: ["apps/workflows-dashboard/**/*.{ts,tsx}"]
---

# Workflows Dashboard Development Rules

## Component Structure
- Use functional components with TypeScript
- Follow feature-based architecture
- Implement container/presenter pattern
- Use compound components when needed
- Keep components focused and small

```typescript
// Container component
export const DataContainer: FunctionComponent = () => {
const logic = useDataLogic();
return <DataPresenter {...logic} />;
};

// Presenter component
export const DataPresenter: FunctionComponent<DataPresenterProps> = (props) => {
return <div>...</div>;
};
```

## Hooks and Business Logic
- Separate business logic into hooks
- Use custom hooks for reusable logic
- Follow the `use` prefix convention
- Keep hooks single-purpose
- Place hooks in feature directories

```typescript
export const useWorkflowLogic = () => {
// Workflow-specific logic
return {
// Hook return values
};
};
```

## State Management
- Use React Query for API state
- Implement Context for shared state
- Use local state for UI elements
- Follow flux architecture
- Keep state normalized

## TypeScript Usage
- Use strict mode
- Define clear interfaces
- Use type inference
- Export types separately
- Use discriminated unions

## Dashboard Components
- Use data visualization libraries
- Implement proper loading states
- Handle empty states
- Show error states
- Use proper grid layouts

## Data Handling
- Use React Query for data fetching
- Implement proper caching
- Handle loading states
- Show error messages
- Type API responses

## Workflow Management
- Implement clear workflow states
- Handle transitions properly
- Show progress indicators
- Validate workflow steps
- Handle edge cases

## Error Handling
- Use error boundaries
- Show user-friendly errors
- Log errors appropriately
- Implement fallbacks
- Handle async errors

## Performance
- Optimize renders
- Use virtualization for lists
- Implement code splitting
- Use lazy loading
- Monitor performance

## Testing
- Write unit tests
- Test workflows thoroughly
- Use integration tests
- Mock API responses
- Test error states

## File Organization
- Use feature folders
- Keep related files together
- Use clear naming
- Implement barrel exports
- Follow consistent structure

## Styling
- Use Tailwind CSS
- Follow design system
- Use CSS variables
- Keep styles maintainable
- Use CSS modules when needed

## Forms
- Use React Hook Form
- Implement validation
- Show feedback
- Handle submissions
- Use controlled inputs

## Documentation
- Document complex logic
- Write clear comments
- Keep docs updated
- Use JSDoc
- Document APIs

## Code Quality
- Follow ESLint rules
- Use consistent style
- Write clear code
- Keep it maintainable
- Review regularly

## Accessibility
- Follow WCAG
- Use semantic HTML
- Add ARIA labels
- Test keyboard nav
- Support screen readers

## Security
- Validate inputs
- Handle auth properly
- Protect sensitive data
- Follow best practices
- Use secure APIs

## Best Practices
- Follow React patterns
- Keep code DRY
- Handle edge cases
- Write clean code
- Review regularly

## Dashboard Specific
- Use proper charts
- Show clear metrics
- Implement filters
- Handle large datasets
- Support sorting

## Workflow Visualization
- Show clear status
- Use proper icons
- Implement transitions
- Show progress
- Handle errors
101 changes: 101 additions & 0 deletions .cursor/rules/workflows-service.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
description: Workflow Service Rules
globs: ["services/workflows-service/**/*.{ts}"]
---
### Code Organization & Structure

1. All service-related code must be organized in feature modules (e.g., workflow, alert, transaction)
2. Each feature module should contain separate files for:
- Service implementation (.service.ts)
- Controller implementation (.controller.ts)
- Integration tests (.intg.test.ts)
- Unit tests (.unit.test.ts)
- DTOs and types (.types.ts)

### Import Guidelines

1. Imports must be organized in the following order with a blank line between groups:
- Node.js built-in modules
- External npm packages
- Internal modules (using @/ alias)
- Relative imports
2. Circular dependencies are strictly prohibited
3. Use the @/ alias for internal imports instead of relative paths
4. Only import what is needed using named imports

### TypeScript Usage

1. Always define explicit return types for functions and methods
2. Use interfaces for object types rather than type aliases where possible
3. Avoid using the `any` type - use `unknown` if type is truly uncertain
4. Use type assertions with 'as' syntax rather than angle brackets
5. Make class member accessibility explicit (public/private/protected)
6. Use TypeScript "as const" for fixed sets of values

### Service Implementation

1. Services must use the @Injectable() decorator
2. Service names must end with 'Service' suffix
3. Dependency injection should be done through constructor parameters
4. Services should handle their own error cases using custom exception classes
5. Use dependency injection tokens in SCREAMING_SNAKE_CASE format

### Testing Standards

1. Test files must follow the naming pattern: *.test.ts for unit tests and *.intg.test.ts for integration tests
2. Each test suite should have a clear describe block indicating the module/function being tested
3. Use 'it' rather than 'test' for test cases
4. Mock external dependencies in unit tests
5. Integration tests should use test databases/containers
6. Test file location should mirror the source file structure
7. Use AAA pattern test structure

### Error Handling

1. Use custom exception classes extending from base NestJS exceptions
2. Error messages should be descriptive and consistent
3. Always include relevant context in error objects
4. Log errors appropriately using the logging service
5. Handle async errors using try/catch blocks

### Documentation

1. Include examples in documentation for complex operations
2. Keep documentation up to date with code changes

### Database Operations

1. Use the PrismaService for database operations
2. Wrap database operations in transactions when multiple operations need to be atomic
3. Use proper error handling for database operations
4. Include proper database indexes for frequently queried fields
5. Always use scope service or add a filter on projectIds in queries

### API Design

1. Use appropriate HTTP methods for operations (GET, POST, PUT, DELETE)
2. Use meaningful route paths that reflect the resource hierarchy
3. Include proper Swagger documentation for all endpoints
4. Return consistent response structures

### Logging

1. Use the provided logging service rather than console.log
2. Include appropriate context with all log messages
3. Use proper log levels (debug, info, warn, error)
4. Include request IDs in logs for traceability

### Configuration Management

1. Use environment variables for configuration
2. Validate environment variables at startup
3. Use proper typing for configuration objects
4. Keep sensitive information in secrets management

### Performance Considerations

1. Implement pagination for list endpoints
2. Use proper indexing for database queries
3. Implement caching where appropriate
4. Handle large datasets efficiently

11 changes: 11 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
node_modules
dist

# Eslint config file itself
.eslintrc.cjs

# Config files
rollup.config.js
babel.config.js

# Config pkg
packages/config
2 changes: 1 addition & 1 deletion .github/actions/build-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/format-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/integration-test-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/lint-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/spell-check-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/test-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
2 changes: 1 addition & 1 deletion .github/actions/unit-test-action/action.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ runs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
75 changes: 67 additions & 8 deletions .github/workflows/build-preview-environment.yml
Original file line number Diff line number Diff line change
@@ -6,20 +6,26 @@ concurrency:

on:
workflow_dispatch:
inputs:
unified-version:
type: string
description: 'Provide Unified image tag that you want to use in this preview env'
default: 'latest'
pull_request:
types: [ labeled, synchronize ]

permissions:
id-token: write
contents: read
pull-requests: read
contents: write
pull-requests: write
packages: write

env:
REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event_name == 'pull_request' && github.event.pull_request.head.ref }}

jobs:
deploy-dev-pr-environment:
if: contains(github.event.pull_request.labels.*.name, 'deploy-pr')
if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }}
@@ -45,7 +51,7 @@ jobs:
run: |
SANITIZED_BRANCH_NAME=$(echo -n "${{ steps.clean-ref.outputs.ref }}" | tr "/" "-")
echo "Sanitized branch name: $SANITIZED_BRANCH_NAME"
TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8)
TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-18 | sed 's/[-/]$//')
echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT;
echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT;
@@ -65,39 +71,92 @@ jobs:
image_name: workflows-service
ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }}
tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }}
file: 'services/workflows-service/Dockerfile'

build-wf-service-ee:
needs: [deploy-dev-pr-environment,build-wf-service]
uses: ./.github/workflows/build-push-docker-images.yml
with:
registry: ghcr.io/${{ github.repository_owner }}
context: services/workflows-service
image_name: workflows-service-ee
ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }}
tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }}
file: 'services/workflows-service/Dockerfile.ee'

build-backoffice:
needs: deploy-dev-pr-environment
needs: [deploy-dev-pr-environment]
uses: ./.github/workflows/build-push-docker-images.yml
with:
registry: ghcr.io/${{ github.repository_owner }}
context: apps/backoffice-v2
image_name: backoffice
ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }}
tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }}
file: 'apps/backoffice-v2/Dockerfile'

build-kyb:
needs: deploy-dev-pr-environment
needs: [deploy-dev-pr-environment]
uses: ./.github/workflows/build-push-docker-images.yml
with:
registry: ghcr.io/${{ github.repository_owner }}
context: apps/kyb-app
image_name: kyb-app
ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }}
tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }}
file: 'apps/kyb-app/Dockerfile'

build-dashboard:
needs: deploy-dev-pr-environment
needs: [deploy-dev-pr-environment]
uses: ./.github/workflows/build-push-docker-images.yml
with:
registry: ghcr.io/${{ github.repository_owner }}
context: apps/workflows-dashboard
image_name: workflows-dashboard
ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }}
tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }}
file: 'apps/workflows-dashboard/Dockerfile'

build-unified-api:
runs-on: ubuntu-latest
needs: [deploy-dev-pr-environment]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{ vars.PREVIEW_OIDC_ROLE }}
aws-region: ${{ vars.PREVIEW_AWS_REGION }}

# Access the secret
- name: Retrieve secret from Secrets Manager
id: get-secret
run: |
secret_value=$(aws secretsmanager get-secret-value --secret-id ${{ vars.PREVIEW_SECRET }} --query 'SecretString' --output text | jq -r '.SUBMODULE_SECRET')
echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_ENV
echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_OUTPUT
- name: Log in to the container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ghcr.io/${{ github.repository_owner }}
username: ${{ github.actor }}
password: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }}

- name: Checkout repository
run: |
docker pull ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ github.event_name == 'workflow_dispatch' && inputs.unified-version || 'latest' }}
docker tag ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ github.event_name == 'workflow_dispatch' && inputs.unified-version || 'latest' }} ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ needs.deploy-dev-pr-environment.outputs.env_name }}
docker push ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ needs.deploy-dev-pr-environment.outputs.env_name }}
deploy-preview:
needs: [deploy-dev-pr-environment,build-wf-service,build-backoffice,build-kyb,build-dashboard]
needs: [deploy-dev-pr-environment,build-wf-service,build-wf-service-ee,build-backoffice,build-kyb,build-dashboard,build-unified-api]
runs-on: ubuntu-latest
steps:
- name: Trigger workflow in another repo
78 changes: 55 additions & 23 deletions .github/workflows/build-push-docker-images.yml
Original file line number Diff line number Diff line change
@@ -23,10 +23,15 @@ on:
required: true
description: "Tag name of the Preview Image"
type: string
file:
required: true
description: "File name for the Preview Image"
type: string

permissions:
id-token: write
contents: write
packages: write
pull-requests: write

jobs:
@@ -40,18 +45,56 @@ jobs:
ref: ${{ inputs.ref }}
fetch-depth: 1
persist-credentials: false
sparse-checkout: |
${{ inputs.context }}
sparse-checkout-cone-mode: true

- name: Get tags
run: git fetch --tags origin
- name: Configure AWS credentials
if: inputs.image_name == 'workflows-service-ee'
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{ vars.PREVIEW_OIDC_ROLE }}
aws-region: ${{ vars.PREVIEW_AWS_REGION }}

# Access the secret
- name: Retrieve secret from Secrets Manager
if: inputs.image_name == 'workflows-service-ee'
id: get-secret
run: |
echo ${{ inputs.image_name }}
secret_value=$(aws secretsmanager get-secret-value --secret-id ${{ vars.PREVIEW_SECRET }} --query 'SecretString' --output text | jq -r '.SUBMODULE_SECRET')
echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_ENV
echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_OUTPUT
- name: Checkout wf-data-migration
id: wf-migration-code
if: inputs.image_name == 'workflows-service-ee'
uses: actions/checkout@v4
with:
repository: ballerine-io/wf-data-migration
token: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }}
ref: dev
fetch-depth: 1
path: services/workflows-service/prisma/data-migrations

- name: Get Latest Commit ID
if: inputs.image_name == 'workflows-service-ee'
id: lastcommit
uses: nmbgeek/github-action-get-latest-commit@main
with:
owner: ${{ github.repository_owner }}
token: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }}
repo: wf-data-migration
branch: dev

# - name: Get tags
# if: ${{ inputs.image_name }} != 'workflows-service-ee'
# run: git fetch --tags origin

- name: Get version
if: ${{ inputs.image_name }} == 'workflows-service'
if: ${{ inputs.image_name == 'workflows-service' }}
id: version
run: |
TAG=$(git tag -l "$(echo ${{ inputs.image_name }}@)*" | sort -V -r | head -n 1)
echo ${{ inputs.image_name }}
git fetch --tags origin
TAG=$(git tag -l "$(echo workflow-service@)*" | sort -V -r | head -n 1)
echo "tag=$TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "TAG=$TAG" >> "$GITHUB_ENV"
@@ -61,7 +104,7 @@ jobs:
- name: Bump version
id: bump-version
if: ${{ inputs.image_name }} == 'workflows-service'
if: ${{ inputs.image_name == 'workflows-service' }}
uses: ./.github/actions/bump-version
with:
tag: ${{ steps.version.outputs.tag }}
@@ -76,7 +119,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
@@ -103,7 +146,7 @@ jobs:
- name: Print docker version outputs
run: |
echo "Metadata: ${{ steps.docker_meta.outputs.tags }}"
if [[ "${{ inputs.image_name }}" == "workflows-service" ]]; then
if [[ "${{ inputs.image_name }}" == "workflows-service" && "${{ inputs.image_name }}" != "workflows-service-ee" ]]; then
echo "sha_short: ${{ steps.version.outputs.sha_short }}"
echo "bump-version-version: ${{ steps.bump-version.outputs.version }}"
echo "bump-version-tag: ${{ steps.bump-version.outputs.tag }}"
@@ -118,17 +161,6 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: ${{ steps.docker_meta.outputs.tags }}
file: ${{ inputs.file }}
build-args: |
${{ inputs.image_name == 'workflows-service' && format('"RELEASE={0}"\n"SHORT_SHA={1}"', steps.version.outputs.tag, steps.version.outputs.sha_short) || '' }}
- name: Scan Docker Image
uses: aquasecurity/trivy-action@master
continue-on-error: true
with:
image-ref: ${{ steps.docker_meta.outputs.tags }}
format: 'table'
ignore-unfixed: true
exit-code: 1
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
timeout: '5m'
${{ (inputs.image_name == 'workflows-service' && format('"RELEASE={0}"\n"SHORT_SHA={1}"', steps.version.outputs.tag, steps.version.outputs.sha_short)) || (inputs.image_name == 'workflows-service-ee' && format('"BASE_IMAGE=ghcr.io/ballerine-io/workflows-service:{0}"', inputs.tag)) || '' }}
20 changes: 5 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -6,10 +6,10 @@ on:
- main
paths:
# Run this pipeline only if there are changes in specified path
- "apps/**"
- "services/**"
- "examples/**"
- "experiments/**"
- 'apps/**'
- 'services/**'
- 'examples/**'
- 'experiments/**'
workflow_call:
workflow_dispatch:

@@ -37,7 +37,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}

steps:
@@ -66,13 +66,3 @@ jobs:

- name: Test
uses: ./.github/actions/test-action
test_windows:
runs-on: windows-latest
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Test
uses: ./.github/actions/unit-test-action
2 changes: 1 addition & 1 deletion .github/workflows/db-ops.yaml
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
82 changes: 82 additions & 0 deletions .github/workflows/deploy-backoffice.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Under Testing - Build and Deploy Backoffice Application

on:
# push:
# paths:
# # Run this pipeline only if there are changes in specified path
# - 'apps/backoffice-v2/**'
# branches:
# - "dev"
workflow_dispatch:
inputs:
environment:
type: choice
description: 'Choose Environment'
required: true
default: 'dev'
options:
- 'dev'
- 'sb'
- 'prod'
workflow_call:
inputs:
environment:
type: string
description: 'Environment'
required: true
default: 'dev'

jobs:
build:
name: Build Backoffice App
runs-on: ubuntu-latest
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
steps:
# Trigger a webhook
- name: Trigger Build webhook
run: |
# curl -X POST -d {} "${{ secrets.BACKOFFICE_WEBHOOK_URL }}" -H "Content-Type:application/json"
response=$(curl -s -w "\n%{http_code}" -X POST -d {} "${{ secrets.BACKOFFICE_WEBHOOK_URL }}" -H "Content-Type:application/json")
status_code=$(echo "$response" | tail -n 1)
if [ "$status_code" -lt 200 ] || [ "$status_code" -ge 300 ]; then
echo "Error: Webhook request failed with status $status_code"
echo "Response: $(echo "$response" | head -n -1)"
exit 1
fi
send-to-slack:
runs-on: ubuntu-latest
needs: [build]
if: ${{ needs.build.result == 'success' }}
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write

steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "Back-office Build initialized in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}

on-failure:
runs-on: ubuntu-latest
needs: [build]
if: failure()
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write
steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "Backoffice Build job failed in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}
82 changes: 82 additions & 0 deletions .github/workflows/deploy-dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Under Testing - Build and Deploy Dashboard Application

on:
# push:
# paths:
# # Run this pipeline only if there are changes in specified path
# - 'apps/workflows-dashboard/**'
# branches:
# - "dev"
workflow_dispatch:
inputs:
environment:
type: choice
description: 'Choose Environment'
required: true
default: 'dev'
options:
- 'dev'
- 'sb'
- 'prod'
workflow_call:
inputs:
environment:
type: string
description: 'Environment'
required: true
default: 'dev'

jobs:
build:
name: Build Dashboard App
runs-on: ubuntu-latest
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
steps:
# Trigger a webhook
- name: Trigger Build webhook
run: |
# curl -X POST -d {} "${{ secrets.DASHBOARD_WEBHOOK_URL }}" -H "Content-Type:application/json"
response=$(curl -s -w "\n%{http_code}" -X POST -d {} "${{ secrets.DASHBOARD_WEBHOOK_URL }}" -H "Content-Type:application/json")
status_code=$(echo "$response" | tail -n 1)
if [ "$status_code" -lt 200 ] || [ "$status_code" -ge 300 ]; then
echo "Error: Webhook request failed with status $status_code"
echo "Response: $(echo "$response" | head -n -1)"
exit 1
fi
send-to-slack:
runs-on: ubuntu-latest
needs: [build]
if: ${{ needs.build.result == 'success' }}
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write

steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "Dashboard Build initialized in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}

on-failure:
runs-on: ubuntu-latest
needs: [build]
if: failure()
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write
steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "Dashboard Build job failed in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}
82 changes: 82 additions & 0 deletions .github/workflows/deploy-kyb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Under Testing - Build and Deploy KYB Application

on:
# push:
# paths:
# # Run this pipeline only if there are changes in specified path
# - 'apps/kyb-app/**'
# branches:
# - "dev"
workflow_dispatch:
inputs:
environment:
type: choice
description: 'Choose Environment'
required: true
default: 'dev'
options:
- 'dev'
- 'sb'
- 'prod'
workflow_call:
inputs:
environment:
type: string
description: 'Environment'
required: true
default: 'dev'

jobs:
build:
name: Build KYB App
runs-on: ubuntu-latest
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
steps:
# Trigger a webhook
- name: Trigger Build webhook
run: |
# curl -X POST -d {} "${{ secrets.KYB_WEBHOOK_URL }}" -H "Content-Type:application/json"
response=$(curl -s -w "\n%{http_code}" -X POST -d {} "${{ secrets.KYB_WEBHOOK_URL }}" -H "Content-Type:application/json")
status_code=$(echo "$response" | tail -n 1)
if [ "$status_code" -lt 200 ] || [ "$status_code" -ge 300 ]; then
echo "Error: Webhook request failed with status $status_code"
echo "Response: $(echo "$response" | head -n -1)"
exit 1
fi
send-to-slack:
runs-on: ubuntu-latest
needs: [build]
if: ${{ needs.build.result == 'success' }}
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write

steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "KYB Build initialized in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}

on-failure:
runs-on: ubuntu-latest
needs: [build]
if: failure()
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
permissions:
contents: read
packages: write
steps:
- name: Send alert to Slack channel
id: slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: '${{ secrets.ARGO_SLACK_CHANNEL_ID }}'
slack-message: "KYB Build job failed in ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}."
env:
SLACK_BOT_TOKEN: ${{ secrets.ARGO_SLACK_BOT_TOKEN }}
5 changes: 3 additions & 2 deletions .github/workflows/deploy-wf-service.yml
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ jobs:
- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
@@ -111,6 +111,7 @@ jobs:
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-${{ inputs.environment }}
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ inputs.environment }}
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest
else
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-dev
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-dev ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-${{ inputs.environment }}
@@ -156,7 +157,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
17 changes: 9 additions & 8 deletions .github/workflows/destroy-preview-environment.yml
Original file line number Diff line number Diff line change
@@ -18,7 +18,12 @@ env:

jobs:
deploy-dev-pr-environment:
if: contains(github.event.pull_request.labels.*.name, 'deploy-pr')
if: |
(github.event_name == 'pull_request' && github.event.action == 'unlabeled' && github.event.label.name == 'deploy-pr')
||
(github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy-pr'))
||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }}
@@ -31,19 +36,13 @@ jobs:
CLEAN_BRANCH_NAME=${BRANCH_NAME#refs/heads/}
echo "ref=$CLEAN_BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Checkout the Tool and actions
uses: actions/checkout@v4
with:
ref: ${{ steps.clean-ref.outputs.ref }}
fetch-depth: 1

- name: "Sanitize ENV name"
id: sanitize_env
shell: bash
run: |
SANITIZED_BRANCH_NAME=$(echo -n ${{ steps.clean-ref.outputs.ref }} | tr "/" "-")
echo "Sanitized branch name: $SANITIZED_BRANCH_NAME"
TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8)
TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-18 | sed 's/[-/]$//')
echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT;
echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT;
@@ -60,6 +59,8 @@ jobs:
(github.event_name == 'pull_request' && github.event.action == 'unlabeled' && github.event.label.name == 'deploy-pr')
||
(github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy-pr'))
||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Trigger workflow in another repo
4 changes: 2 additions & 2 deletions .github/workflows/hotfix-wf-service.yml
Original file line number Diff line number Diff line change
@@ -77,7 +77,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
@@ -193,7 +193,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
41 changes: 41 additions & 0 deletions .github/workflows/packer-build-ami.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Packer build AWS AMI's
on:
workflow_dispatch:

jobs:
plan:
environment: Terraform
defaults:
run:
working-directory: /home/runner/work/ballerine/deploy/aws_ami
runs-on: ubuntu-latest
name: Packer build Artifacts
steps:
- name: Checkout to Git
uses: actions/checkout@v2

- name: Assume Role
uses: ./
env:
ROLE_ARN: ${{ secrets.AWS_PACKER_ROLE }}
ROLE_SESSION_NAME: packersession
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
DURATION_SECONDS: 900

- name: Setup `packer`
uses: hashicorp/setup-packer@main
id: setup
with:
version: 1.8.7

- name: Run `packer init`
id: init
run: "packer init template.json.pkr.hcl"

- name: Run `packer validate`
id: validate
run: "packer validate template.json.pkr.hcl"

- name: Build AWS AMIs
run: "packer build template.json.pkr.hcl"
2 changes: 1 addition & 1 deletion .github/workflows/publish-workflows-service.yml
Original file line number Diff line number Diff line change
@@ -339,7 +339,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
2 changes: 1 addition & 1 deletion .github/workflows/push-workflows-service-image.yml
Original file line number Diff line number Diff line change
@@ -76,7 +76,7 @@ jobs:

- name: Cache Docker layers
id: cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
60 changes: 60 additions & 0 deletions .github/workflows/test-ballerine-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Test Ballerine Deploy

on:
workflow_dispatch:
push:
branches: [ dev ]

jobs:
test-deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'

- name: Start containers
run: |
sudo apt-get update
sudo apt-get install docker-compose
cd deploy
docker-compose up -d
- name: Wait for containers to be healthy
run: |
cd deploy
timeout=180 # 3 minutes timeout
elapsed=0
interval=10
while [ $elapsed -lt $timeout ]; do
if docker-compose ps | grep -q "healthy"; then
unhealthy_count=$(docker-compose ps | grep -c "unhealthy" || true)
if [ $unhealthy_count -eq 0 ]; then
echo "All containers are healthy!"
exit 0
fi
fi
echo "Waiting for containers to be healthy... ($elapsed seconds elapsed)"
sleep $interval
elapsed=$((elapsed + interval))
done
echo "Timeout reached. Some containers are not healthy."
docker-compose ps
docker-compose logs
exit 1
- name: Clean up
if: always()
run: |
cd deploy
docker-compose down -v
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
21
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@

<a href="https://docs.ballerine.com/">Documentation</a>
·
<a href="https://join.slack.com/t/ballerine-oss/shared_invite/zt-1iu6otkok-OqBF3TrcpUmFd9oUjNs2iw">Slack</a>
<a href="https://join.slack.com/t/ballerine-oss/shared_invite/zt-1il7txerq-K0YrXtlzMttGgD3XXYxlfw">Slack</a>
·
<a href="https://www.ballerine.com/">Website</a>
·
@@ -74,10 +74,11 @@ We believe in enabling companies to manage user identity and risk according to
**Parts of the system you might look for but are not in THIS demo:**
- Our Rule Engine is still under construction and will soon be released.


**Getting started**
To set up a local environment, follow these steps:
1. #### Install prerequisites:
- Node.js ([Install NVM](https://github.com/nvm-sh/nvm), then install node "nvm install --lts")
- Node.js ([Install NVM](https://github.com/nvm-sh/nvm), then install node "nvm install 21")
- Latest PNPM version ([Install PNPM](https://pnpm.io/installation))
- Docker and docker compose ([Docker](https://docs.docker.com/desktop), [Docker Compose](https://docs.docker.com/compose/install))

@@ -127,7 +128,7 @@ Once the process is complete, _2 tabs_ will open in your browser:
- The Back Office case will update as you progress
3. **Review & Process**
- Once complete, the case status changes to "manual review"
- Once complete, the case status changes to "manual review"
- Assign the case to yourself
- Choose to: Approve, Reject, or Request Resubmission
@@ -143,7 +144,7 @@ Once the process is complete, _2 tabs_ will open in your browser:
We appreciate all types of contributions and believe that an active community is the secret to a rich and stable product.
Here are some of the ways you can contribute:
- Give us feedback in our [Slack community](https://join.slack.com/t/ballerine-oss/shared_invite/zt-1iu6otkok-OqBF3TrcpUmFd9oUjNs2iw)
- Give us feedback in our [Slack community](https://join.slack.com/t/ballerine-oss/shared_invite/zt-1il7txerq-K0YrXtlzMttGgD3XXYxlfw)
- Help with bugs and features on [our Issues page](https://github.com/ballerine-io/ballerine/issues)
- Submit a [feature request](https://github.com/ballerine-io/ballerine/issues/new?assignees=&labels=enhancement%2C+feature&template=feature_request.md) or [bug report](https://github.com/ballerine-io/ballerine/issues/new?assignees=&labels=bug&template=bug_report.md)
1 change: 1 addition & 0 deletions apps/backoffice-v2/.env.example
Original file line number Diff line number Diff line change
@@ -6,3 +6,4 @@ VITE_POLLING_INTERVAL=10
VITE_ASSIGNMENT_POLLING_INTERVAL=5
VITE_FETCH_SIGNED_URL=false
VITE_ENVIRONMENT_NAME=local
MODE=development
7 changes: 4 additions & 3 deletions apps/backoffice-v2/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -6,11 +6,12 @@ module.exports = {
callees: ['ctw'],
},
},
parserOptions: {
project: './tsconfig.eslint.json',
},
rules: {
'tailwindcss/no-custom-classname': 'off',
'tailwindcss/classnames-order': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.eslint.json',
},
};
8 changes: 8 additions & 0 deletions apps/backoffice-v2/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -18,5 +18,13 @@ const config: StorybookConfig = {
docs: {
autodocs: true,
},
viteFinal: config => {
config.optimizeDeps = {
...config.optimizeDeps,
include: ['@ballerine/ui'],
};

return config;
},
};
export default config;
410 changes: 410 additions & 0 deletions apps/backoffice-v2/CHANGELOG.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions apps/backoffice-v2/Dockerfile
Original file line number Diff line number Diff line change
@@ -22,10 +22,18 @@ CMD ["npm", "run", "dev", "--host"]

FROM nginx:stable-alpine as prod

WORKDIR /app

COPY --from=dev /app/dist /usr/share/nginx/html

COPY --from=dev /app/entrypoint.sh /app/entrypoint.sh

COPY example.nginx.conf /etc/nginx/conf.d/default.conf

RUN chmod a+x /app/entrypoint.sh;

EXPOSE 80

ENTRYPOINT [ "/app/entrypoint.sh" ]

CMD ["nginx", "-g", "daemon off;"]
53 changes: 53 additions & 0 deletions apps/backoffice-v2/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env sh

if [[ -n "$VITE_DOMAIN" ]]
then
VITE_API_URL="$VITE_DOMAIN/api/v1/internal"
fi

if [[ -n "$VITE_API_KEY" ]]
then
VITE_API_KEY="$VITE_API_KEY"
fi

if [[ -n "$VITE_AUTH_ENABLED" ]]
then
VITE_AUTH_ENABLED="$VITE_AUTH_ENABLED"
fi

if [[ -n "$VITE_MOCK_SERVER" ]]
then
VITE_MOCK_SERVER="$VITE_MOCK_SERVER"
fi

if [[ -n "$VITE_POLLING_INTERVAL" ]]
then
VITE_POLLING_INTERVAL="$VITE_POLLING_INTERVAL"
fi

if [[ -n "$VITE_ASSIGNMENT_POLLING_INTERVAL" ]]
then
VITE_ASSIGNMENT_POLLING_INTERVAL="$VITE_ASSIGNMENT_POLLING_INTERVAL"
fi

if [[ -n "$VITE_FETCH_SIGNED_URL" ]]
then
VITE_FETCH_SIGNED_URL="$VITE_FETCH_SIGNED_URL"
fi

cat << EOF > /usr/share/nginx/html/config.js
globalThis.env = {
VITE_API_URL: "$VITE_API_URL",
VITE_API_KEY: "$VITE_API_KEY",
VITE_AUTH_ENABLED: "$VITE_AUTH_ENABLED",
VITE_MOCK_SERVER: "$VITE_MOCK_SERVER",
VITE_POLLING_INTERVAL: "$VITE_POLLING_INTERVAL",
VITE_ASSIGNMENT_POLLING_INTERVAL: "$VITE_ASSIGNMENT_POLLING_INTERVAL",
VITE_FETCH_SIGNED_URL: "$VITE_FETCH_SIGNED_URL",
VITE_ENVIRONMENT_NAME: "local",
MODE: "production"
}
EOF

# Handle CMD command
exec "$@"
3 changes: 3 additions & 0 deletions apps/backoffice-v2/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare global {
export var env: { [key: string]: any };
}
1 change: 1 addition & 0 deletions apps/backoffice-v2/index.html
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ballerine - Backoffice</title>
<script type="text/javascript" src="/config.js"></script>
<script>
let cachedTheme = localStorage.getItem('theme');
const themes = ['dark', 'light'];
28 changes: 17 additions & 11 deletions apps/backoffice-v2/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ballerine/backoffice-v2",
"version": "0.7.82",
"version": "0.7.124",
"description": "Ballerine - Backoffice",
"homepage": "https://github.com/ballerine-io/ballerine",
"type": "module",
@@ -42,6 +42,7 @@
"start": "vite",
"dev": "vite",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=32768 vite build",
"prod:next": "vite build && vite --host",
"test": "vitest run --passWithNoTests",
"test:unit": "vitest run --passWithNoTests",
"test:e2e": "playwright test",
@@ -51,12 +52,12 @@
"preview": "vite preview"
},
"dependencies": {
"@ballerine/blocks": "0.2.30",
"@ballerine/common": "0.9.59",
"@ballerine/react-pdf-toolkit": "^1.2.51",
"@ballerine/ui": "^0.5.51",
"@ballerine/workflow-browser-sdk": "0.6.78",
"@ballerine/workflow-node-sdk": "0.6.78",
"@ballerine/blocks": "0.2.39",
"@ballerine/common": "0.9.84",
"@ballerine/react-pdf-toolkit": "^1.2.97",
"@ballerine/ui": "0.7.124",
"@ballerine/workflow-browser-sdk": "0.6.106",
"@ballerine/workflow-node-sdk": "0.6.106",
"@botpress/webchat": "^2.1.10",
"@botpress/webchat-generator": "^0.2.9",
"@fontsource/inter": "^4.5.15",
@@ -83,7 +84,6 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@react-pdf/renderer": "^3.1.14",
"@rjsf/utils": "^5.9.0",
"@saola.ai/browser": "^1.1.7",
"@sentry/react": "^7.77.0",
"@tanstack/react-query": "^4.19.1",
"@tanstack/react-table": "^8.9.2",
@@ -112,16 +112,20 @@
"eslint-plugin-tailwindcss": "^3.8.0",
"face-api.js": "^0.22.2",
"framer-motion": "^8.3.4",
"html2canvas-pro": "^1.5.8",
"i18next": "^22.4.9",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.1.1",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.8.4",
"leaflet": "^1.9.4",
"libphonenumber-js": "^1.10.49",
"lodash-es": "^4.17.21",
"lowlight": "^3.1.0",
"lucide-react": "0.445.0",
"match-sorter": "^6.3.1",
"msw": "^1.0.0",
"papaparse": "^5.5.1",
"posthog-js": "^1.154.2",
"qs": "^6.11.2",
"react": "^18.2.0",
@@ -136,10 +140,11 @@
"react-leaflet": "^4.2.1",
"react-medium-image-zoom": "^5.2.10",
"react-router-dom": "^6.11.2",
"react-to-pdf": "^1.0.1",
"react-zoom-pan-pinch": "^3.0.8",
"recharts": "^2.7.2",
"sonner": "^1.4.3",
"string-ts": "^1.2.0",
"string-ts": "1.3.0",
"tailwind-merge": "^1.10.0",
"tailwindcss-animate": "^1.0.5",
"tesseract.js": "^4.0.1",
@@ -148,8 +153,8 @@
"zod": "^3.23.4"
},
"devDependencies": {
"@ballerine/config": "^1.1.28",
"@ballerine/eslint-config-react": "^2.0.28",
"@ballerine/config": "^1.1.37",
"@ballerine/eslint-config-react": "^2.0.37",
"@cspell/cspell-types": "^6.31.1",
"@faker-js/faker": "^7.6.0",
"@playwright/test": "^1.32.1",
@@ -171,6 +176,7 @@
"@types/leaflet": "^1.9.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.11.13",
"@types/papaparse": "^5.3.15",
"@types/qs": "^6.9.7",
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5",
File renamed without changes.
24 changes: 24 additions & 0 deletions apps/backoffice-v2/public/locales/en/toast.json
Original file line number Diff line number Diff line change
@@ -86,6 +86,14 @@
"empty_extraction": "Unable to extract the document's relevant fields.",
"error": "Failed to perform OCR on the document."
},
"business_monitoring_off": {
"success": "Merchant monitoring has been turned off successfully.",
"error": "Error occurred while turning merchant monitoring off."
},
"business_monitoring_on": {
"success": "Merchant monitoring has been turned on successfully.",
"error": "Error occurred while turning merchant monitoring on."
},
"business_report_creation": {
"success": "Merchant check created successfully.",
"error": "Error occurred while creating a merchant check.",
@@ -97,12 +105,28 @@
"error": "Error occurred while creating merchant checks.",
"is_example": "Please contact Ballerine at oss@ballerine.com for access to this feature."
},
"business_report_status_update": {
"success": "Merchant check status updated successfully.",
"error": "Error occurred while updating merchant check status."
},
"note_created": {
"success": "Note added successfully.",
"error": "Error occurred while adding note."
},
"update_details": {
"success": "Details updated successfully.",
"error": "Error occurred while updating details."
},
"ubo_created": {
"success": "UBO successfully added",
"error": "Error adding UBO"
},
"ubo_deleted": {
"success": "UBO successfully removed",
"error": "Error removing UBO"
},
"request_documents": {
"success": "Documents requested successfully.",
"error": "Error occurred while requesting documents."
}
}
11 changes: 11 additions & 0 deletions apps/backoffice-v2/src/@types/react-table.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import '@tanstack/react-table';

declare module '@tanstack/react-table' {
import { RowData } from '@tanstack/react-table';

interface ColumnMeta<TData extends RowData, TValue> {
useWrapper?: boolean;
}
}

export {};
19 changes: 0 additions & 19 deletions apps/backoffice-v2/src/Router/types.ts

This file was deleted.

8 changes: 4 additions & 4 deletions apps/backoffice-v2/src/common/api-client/interfaces.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import { z, ZodSchema } from 'zod';
export interface IApiClient {
<TBody extends AnyRecord, TZodSchema extends ZodSchema>(params: {
endpoint: string;
method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH;
method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH | typeof Method.DELETE;
body?: TBody;
options?: Omit<RequestInit, 'body'>;
timeout?: number;
@@ -19,7 +19,7 @@ export interface IApiClient {

<TBody extends AnyRecord, TZodSchema extends ZodSchema>(params: {
url: string;
method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH;
method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH | typeof Method.DELETE;
body?: TBody;
options?: Omit<RequestInit, 'body'>;
timeout?: number;
@@ -30,7 +30,7 @@ export interface IApiClient {

<TZodSchema extends ZodSchema>(params: {
endpoint: string;
method: typeof Method.GET | typeof Method.DELETE;
method: typeof Method.GET;
options?: Omit<RequestInit, 'body'>;
timeout?: number;
schema: TZodSchema;
@@ -39,7 +39,7 @@ export interface IApiClient {

<TZodSchema extends ZodSchema>(params: {
url: string;
method: typeof Method.GET | typeof Method.DELETE;
method: typeof Method.GET;
options?: Omit<RequestInit, 'body'>;
timeout?: number;
schema: TZodSchema;
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { cva, VariantProps } from 'class-variance-authority';
import { ctw } from '../../../utils/ctw/ctw';

export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:text-primary-foreground disabled:bg-slate-400 disabled:opacity-50 disabled:pointer-events-none disabled:shadow-none ring-offset-background',
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-30 disabled:pointer-events-none disabled:shadow-none ring-offset-background',
{
variants: {
variant: {
@@ -18,7 +18,12 @@ export const buttonVariants = cva(
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
status: 'focus-visible:ring-0 focus-visible:ring-offset-0 focus:!bg-[#F4F6FD] bg-[#F4F6FD]',
link: 'underline-offset-4 hover:underline text-primary',
'wp-primary':
'bg-wp-primary text-wp-primary-foreground hover:bg-wp-primary/90 disabled:text-wp-primary-foreground disabled:bg-wp-primary',
'wp-outline':
'border border-wp-primary text-wp-primary hover:bg-wp-primary hover:text-wp-primary-foreground',
},
size: {
default: 'h-10 py-2 px-4',
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactNode, useCallback, useState } from 'react';
import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons';
import { ReactNode, useCallback } from 'react';
import {
Badge,
Button,
@@ -8,13 +9,14 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandLoading,
CommandSeparator,
ctw,
Popover,
PopoverContent,
PopoverTrigger,
} from '@ballerine/ui';
import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons';

import { Separator } from '@/common/components/atoms/Separator/Separator';

interface IMultiSelectProps<
@@ -25,10 +27,24 @@ interface IMultiSelectProps<
},
> {
title: string;
isLoading?: boolean;
selectedValues: Array<TOption['value']>;
onSelect: (value: Array<TOption['value']>) => void;
onClearSelect: () => void;
options: TOption[];
props?: {
content?: {
className?: string;
};
trigger?: {
leftIcon?: JSX.Element;
rightIcon?: JSX.Element;
className?: string;
title?: {
className?: string;
};
};
};
}

export const MultiSelect = <
@@ -39,32 +55,37 @@ export const MultiSelect = <
},
>({
title,
selectedValues,
isLoading,
selectedValues: selected,
onSelect,
onClearSelect,
options,
props,
}: IMultiSelectProps<TOption>) => {
const [selected, setSelected] = useState(selectedValues);

const onSelectChange = useCallback(
(value: TOption['value']) => {
const isSelected = selected.some(selectedValue => selectedValue === value);
const nextSelected = isSelected
? selected.filter(selectedValue => selectedValue !== value)
: [...selected, value];

setSelected(nextSelected);
onSelect(nextSelected);
},
[onSelect, selected],
);

const TriggerLeftIcon = props?.trigger?.leftIcon ?? <PlusCircledIcon className="mr-2 h-4 w-4" />;

return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border">
<PlusCircledIcon className="mr-2 h-4 w-4" />
{title}
<Button
variant="outline"
size="sm"
className={ctw(`h-8 border`, props?.trigger?.className)}
>
{TriggerLeftIcon}
<span className={ctw(props?.trigger?.title?.className)}>{title}</span>
{selected?.length > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
@@ -81,9 +102,9 @@ export const MultiSelect = <
.filter(option => selected.some(value => value === option.value))
.map(option => (
<Badge
key={`${option.value}`}
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
className="max-w-[20ch] truncate rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
@@ -92,52 +113,63 @@ export const MultiSelect = <
</div>
</>
)}
{props?.trigger?.rightIcon}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<PopoverContent className={ctw(`w-[200px] p-0`, props?.content?.className)} align="start">
<Command filter={(value, search) => (value.includes(search) ? 1 : 0)}>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map(option => {
const isSelected = selected.some(value => value === option.value);
{isLoading && (
<CommandLoading className={`flex items-center justify-center pb-3 text-sm`}>
Loading...
</CommandLoading>
)}
{!isLoading && options.length === 0 && <CommandEmpty>No results found.</CommandEmpty>}
{!isLoading && options.length > 0 && (
<CommandGroup>
<div className={`max-h-[250px] overflow-y-auto`}>
{options.map(option => {
const isSelected = selected.some(value => value === option.value);

return (
<CommandItem key={option.value} onSelect={() => onSelectChange(option.value)}>
<div
className={ctw(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)}
>
<CheckIcon className={ctw('h-4 w-4')} />
</div>
{option.icon}
<span>{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
{selected?.length > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => {
onClearSelect();
setSelected([]);
}}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
return (
<CommandItem
value={option.label}
key={`${option.value}`}
className={`cursor-pointer`}
onSelect={() => onSelectChange(option.value)}
>
<div
className={ctw(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)}
>
<CheckIcon className={ctw('h-4 w-4')} />
</div>
{option.icon}
<span>{option.label}</span>
</CommandItem>
);
})}
</div>
</CommandGroup>
)}
</CommandList>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={onClearSelect}
className={ctw(
`cursor-pointer justify-center text-center`,
selected.length === 0 && 'pointer-events-none opacity-50',
)}
>
Clear filters
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={ctw(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { cva } from 'class-variance-authority';
import styles from './Sheet.module.css';

export const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out animate-in animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
Original file line number Diff line number Diff line change
@@ -4,8 +4,8 @@ import { ctw } from '../../../utils/ctw/ctw';
import React, { ComponentProps } from 'react';

interface IUserAvatarProps extends Omit<ComponentProps<typeof Avatar>, 'src' | 'alt'> {
fullName: string;
avatarUrl: string | undefined;
fullName?: string | null;
avatarUrl?: string | null;
}

export const UserAvatar: React.FC<IUserAvatarProps> = ({
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Skeleton } from '@ballerine/ui';
import { FunctionComponent } from 'react';
import { Card } from '@/common/components/atoms/Card/Card';
import { CardContent } from '@/common/components/atoms/Card/Card.Content';

export interface CaseVideoGuideProps {
title?: string;
description?: string;
thumbnailUrl?: string;
videoSrc?: string;
}

export const CaseVideoGuide: FunctionComponent<CaseVideoGuideProps> = ({
title = 'Onboarding Introduction',
description = 'Learn about Ballerine complete onboarding and underwriting capabilities',
videoSrc = 'https://www.loom.com/embed/efb8b2fdadef4fa79b4f8b412f1e4f4d?sid=02b86870-55ba-4914-bb59-24ba02121852&hideEmbedTopBar=true',
}) => {
return (
<Card className="col-span-1 h-full xl:col-span-2">
<CardContent className="p-4">
<div className="flex h-full flex-row gap-6">
{/* Title and text section */}
<div className="flex w-1/3 flex-col">
<h3 className="mb-3 text-lg font-bold">{title}</h3>
<p className="text-sm text-gray-700">{description}</p>
</div>

<div className="relative w-2/3 overflow-hidden rounded-md">
<div className="relative h-full">
<div
id="case-video-iframe"
style={{
position: 'relative',
paddingBottom: '56.25%',
height: 0,
}}
>
<Skeleton className="size-full absolute inset-0" />
<iframe
src={videoSrc}
frameBorder="0"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
/>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -8,29 +8,35 @@ import { Calendar } from '../../organisms/Calendar/Calendar';
type TDateRangePickerProps = {
onChange: NonNullable<ComponentProps<typeof Calendar>['onSelect']>;
value: NonNullable<ComponentProps<typeof Calendar>['selected']>;
placeholder?: string;
className?: ComponentProps<'div'>['className'];
};

export const DateRangePicker = ({ onChange, value, className }: TDateRangePickerProps) => {
export const DateRangePicker = ({
onChange,
value,
placeholder,
className,
}: TDateRangePickerProps) => {
return (
<div className={ctw('grid gap-2', className)}>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={'outline'}
className={ctw('w-[300px] justify-start text-left font-normal', {
className={ctw('h-8 w-[250px] justify-start text-left font-normal', {
'text-muted-foreground': !value,
})}
>
<CalendarIcon className="size-4 mr-2" />
<CalendarIcon className="mr-2 d-4" />
{value?.from && value?.to && (
<>
{formatDate(value.from, 'LLL dd, y')} - {formatDate(value.to, 'LLL dd, y')}
</>
)}
{value?.from && !value?.to && formatDate(value.from, 'LLL dd, y')}
{!value?.from && !value?.to && <span>Pick a date</span>}
{!value?.from && !value?.to && <span>{placeholder ?? 'Pick a date'}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { ctw } from '@ballerine/ui';
import { t } from 'i18next';
import { ArrowRightIcon } from 'lucide-react';
import { ComponentPropsWithoutRef } from 'react';
import { Link } from 'react-router-dom';

import { Button } from '@/common/components/atoms/Button/Button';
import { UserAvatar } from '@/common/components/atoms/UserAvatar/UserAvatar';
import { env } from '@/common/env/env';
import { useLocale } from '@/common/hooks/useLocale/useLocale';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { getDemoStateErrorText } from './getDemoStateErrorText';

export type ExperienceBallerineCardProps = {
firstName?: string | null;
fullName?: string | null;
avatarUrl?: string | null;
onClick?: () => void;
className?: string;
};

const CreateReportButtonBase = ({
className,
...props
}: ComponentPropsWithoutRef<typeof Button>) => (
<Button
variant="wp-outline"
role="link"
className={ctw('space-x-2 self-start text-base', className)}
{...props}
/>
);

const CreateReportButton = ({
onClick,
locale,
...props
}: Pick<ExperienceBallerineCardProps, 'onClick'> &
ComponentPropsWithoutRef<typeof Button> & {
locale: ReturnType<typeof useLocale>;
}) => {
const buttonContent = (
<>
<span>Create a report</span>
<ArrowRightIcon className="d-4" />
</>
);

if (onClick) {
return (
<CreateReportButtonBase onClick={onClick} {...props}>
{buttonContent}
</CreateReportButtonBase>
);
}

return (
<CreateReportButtonBase asChild {...props}>
<Link to={`/${locale}/merchant-monitoring?isCreating=true`}>{buttonContent}</Link>
</CreateReportButtonBase>
);
};

export const ExperienceBallerineCard = ({
firstName,
fullName,
avatarUrl,
className,
onClick,
}: ExperienceBallerineCardProps) => {
const { data: customer } = useCustomerQuery();
const locale = useLocale();

const { reportsLeft, demoDaysLeft } = customer?.config?.demoAccessDetails ?? {};
const error = getDemoStateErrorText({ reportsLeft, demoDaysLeft });

return (
<div
className={ctw(
'flex flex-col justify-between gap-2 rounded-md border border-gray-300 bg-white px-6 py-4 shadow-sm',
className,
)}
>
<div className={`flex items-center gap-2`}>
{avatarUrl && <UserAvatar fullName={fullName} className={`!d-7`} avatarUrl={avatarUrl} />}
<h3 className={`text-xl font-semibold`}>
{t(`home.greeting`)}
{firstName && ` ${firstName}`}
</h3>
{!error && <span className="ml-2 text-destructive">{demoDaysLeft} days left</span>}
</div>

<p className="leading-loose">
{error ? (
<>
<span className="text-destructive">{error}</span>
<br />
To continue using the system,{' '}
<span className="font-semibold">book a quick call with us.</span>
</>
) : (
<>
💎 You have <span className="font-semibold">{reportsLeft} Web Presence reports</span>{' '}
available.
<br />
Get started now! 🚀
</>
)}
</p>

<CreateReportButton
className="aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-60"
onClick={onClick}
locale={locale}
aria-disabled={!!error}
tabIndex={error ? -1 : 0}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ctw } from '@ballerine/ui';
import { ArrowRightIcon, CrownIcon } from 'lucide-react';

import { Button } from '@/common/components/atoms/Button/Button';
import { BALLERINE_CALENDLY_LINK } from '@/common/constants';
import dashboardImage from './dashboard.png';

export type GetFullAccessCardProps = {
className?: string;
};

export const GetFullAccessCard = ({ className }: GetFullAccessCardProps) => {
return (
<div
className={ctw(
'relative overflow-hidden rounded-md border border-wp-primary px-6 py-4',
className,
)}
>
<div className="shrink-0 space-y-4">
<div className="flex items-center gap-2">
<CrownIcon className="rounded-full bg-wp-primary/30 p-[6px] font-bold text-wp-primary d-7" />
<span className="text-lg font-medium">Get Full Access / Learn More</span>
</div>

<p className="w-3/5 leading-relaxed 2xl:w-1/2">
Get unlimited access to Ballerine, for smarter onboarding and monitoring decisions.
</p>

<Button asChild variant="wp-primary" className="justify-start space-x-2 text-base">
<a href={BALLERINE_CALENDLY_LINK} target="_blank" rel="noreferrer">
<span>Book a quick call</span>
<ArrowRightIcon className="d-4" />
</a>
</Button>
</div>

<div className="absolute -right-24 top-12 2xl:-right-4 2xl:top-6">
<img src={dashboardImage} alt="Dashboard image" className="h-full" />
</div>

<div
className="pointer-events-none absolute inset-0"
style={{
background:
'linear-gradient(135deg, rgba(88, 78, 197, 0.22) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.1) 75%, rgba(88, 78, 197, 0.22) 92%)',
}}
/>
</div>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isNumber } from 'lodash-es';

export const getDemoStateErrorText = ({
reportsLeft,
demoDaysLeft,
}: {
reportsLeft: number | null | undefined;
demoDaysLeft: number | null | undefined;
}) => {
if (isNumber(demoDaysLeft) && demoDaysLeft <= 0) {
return 'Your trial period has expired.';
}

if (isNumber(reportsLeft) && reportsLeft <= 0) {
return 'You have used all of your Web Presence reports credits.';
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
AccordionCard,
Button,
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@ballerine/ui';
import { HelpCircle, SendIcon } from 'lucide-react';
import { FunctionComponent } from 'react';

import { Icon } from './constants';
import { useDocumentTracker } from './hooks/useDocumentTracker';
import { DocumentTrackerItems } from './components/DocumentTrackerItems/DocumentTrackerItems';
import { Dialog } from '@/common/components/organisms/Dialog/Dialog';
import { DialogContent } from '@/common/components/organisms/Dialog/Dialog.Content';
import { DialogDescription } from '@/common/components/organisms/Dialog/Dialog.Description';
import { DialogFooter } from '@/common/components/organisms/Dialog/Dialog.Footer';
import { DialogHeader } from '@/common/components/organisms/Dialog/Dialog.Header';
import { DialogTitle } from '@/common/components/organisms/Dialog/Dialog.Title';
import { DialogTrigger } from '@/common/components/organisms/Dialog/Dialog.Trigger';

export const DocumentTracker: FunctionComponent<{ workflowId: string }> = ({ workflowId }) => {
const {
getSubItems,
selectedIdsToRequest,
onRequestDocuments,
open,
onOpenChange,
isRequestButtonDisabled,
documentTrackerItems,
} = useDocumentTracker({ workflowId });

return (
<div className={`max-w-xs`}>
<AccordionCard className={`h-full`}>
<AccordionCard.Title
className={`flex-row items-center justify-between space-x-2`}
rightChildren={
selectedIdsToRequest.length > 0 ? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>

<DialogContent
onPointerDownOutside={e => e.preventDefault()}
className="px-20 py-12 sm:max-w-2xl"
>
<DialogHeader>
<DialogTitle className="text-4xl">Ask for all requests</DialogTitle>
</DialogHeader>

<DialogDescription>
By clicking the button below, an email with a link will be sent to the customer,
directing them to upload the documents you have marked as requested. The case’s
status will then change to “Revisions” until the customer will provide the
needed documents and fixes.
</DialogDescription>

<DialogFooter>
<Button type="button" onClick={onRequestDocuments}>
Send email
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<HoverCard openDelay={0}>
<HoverCardTrigger className={`pb-1.5 pt-1`}>
<HelpCircle size={18} className={`stroke-slate-400/70`} />
</HoverCardTrigger>
<HoverCardContent side={'top'} align={'start'}>
<ul className={`flex flex-col space-y-2`}>
<li className={`flex items-center gap-x-2`}>
{Icon.INDICATOR}
Not yet provided
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.CHECK}
Provided
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.MARKED}
Marked as requested
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.REQUESTED}
Requested
</li>
</ul>
</HoverCardContent>
</HoverCard>
)
}
>
Documents
</AccordionCard.Title>
<AccordionCard.Content>
<DocumentTrackerItems
documentTrackerItems={documentTrackerItems}
getSubItems={getSubItems}
/>
</AccordionCard.Content>
</AccordionCard>
</div>
);
};

DocumentTracker.displayName = 'DocumentTracker';
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
DropdownMenu,
Input,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Label,
Button,
} from '@ballerine/ui';

import { Upload } from 'lucide-react';
import { FilePlus2 } from 'lucide-react';
import { useState } from 'react';
import { MoreVertical } from 'lucide-react';
import { Dialog } from '@/common/components/organisms/Dialog/Dialog';
import { DialogContent } from '@/common/components/organisms/Dialog/Dialog.Content';
import { DialogDescription } from '@/common/components/organisms/Dialog/Dialog.Description';
import { DialogFooter } from '@/common/components/organisms/Dialog/Dialog.Footer';
import { DialogHeader } from '@/common/components/organisms/Dialog/Dialog.Header';
import { DialogTitle } from '@/common/components/organisms/Dialog/Dialog.Title';
import { DialogTrigger } from '@/common/components/organisms/Dialog/Dialog.Trigger';
import { DialogClose } from '@radix-ui/react-dialog';

type DocumentTrackerItemOptionsProps = {
onMarkChange: (reason?: string) => void;
isDisabled: boolean;
};

export const DocumentTrackerItemOptions = ({
onMarkChange,
isDisabled,
}: DocumentTrackerItemOptionsProps) => {
const [reasonValue, setReasonValue] = useState('');

return (
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="invisible ms-auto text-muted-foreground d-5 focus-visible:visible group-hover:visible aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:bg-background aria-disabled:opacity-50 data-[state=open]:visible"
aria-disabled={isDisabled}
>
<MoreVertical size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="px-0">
<DropdownMenuItem className="w-full px-8 py-1" asChild>
<DialogTrigger asChild>
<Button type="button" variant={'ghost'} className="justify-start px-2">
<FilePlus2 size={16} className="me-2" />
Request from client
</Button>
</DialogTrigger>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent className="px-16 py-12 sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="mb-4 text-2xl">Request document from the client</DialogTitle>
<DialogDescription className="text-base text-primary">
By clicking the &quot;Mark for Request&quot;, the document will be marked as requested.
<br />
Once marked, you can use the &quot;Request&quot; button button at the top of the
documents list to send an email to the customer, asking to upload all of the documents
you have marked as needed.
</DialogDescription>
</DialogHeader>

<Label htmlFor="reason" className="my-2 font-bold">
Reason (Optional)
</Label>
<Input
value={reasonValue}
onChange={e => setReasonValue(e.target.value)}
id="reason"
placeholder="Add reason"
/>
<p>
Use the reason input to tell the client why they are required to upload this document. The
reason will be visible to the client on the data collection flow on the document uploader
</p>

<DialogFooter>
<DialogClose asChild>
<Button onClick={() => onMarkChange(reasonValue)}>Mark for Request</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TDocumentsTrackerItem } from '@/domains/documents/schemas';
import { AccordionCard } from '@ballerine/ui';
import { memo, useMemo } from 'react';

type TDocumentTrackerItemsProps = {
documentTrackerItems: TDocumentsTrackerItem | null | undefined;
getSubItems: (
doc: TDocumentsTrackerItem['business'][number],
) => Parameters<typeof AccordionCard.Item>[number]['subitems'][number];
};

export const DocumentTrackerItems = memo(
({ documentTrackerItems, getSubItems }: TDocumentTrackerItemsProps) => {
const businessSubitems = useMemo(
() => documentTrackerItems?.business.map(getSubItems).filter(Boolean) ?? [],
[documentTrackerItems?.business, getSubItems],
);
const individualsSubitems = useMemo(
() =>
[
...(documentTrackerItems?.individuals.ubos ?? []),
...(documentTrackerItems?.individuals.directors ?? []),
]
.map(getSubItems)
.filter(Boolean),
[
documentTrackerItems?.individuals.ubos,
documentTrackerItems?.individuals.directors,
getSubItems,
],
);

return (
<>
<AccordionCard.Item
title="Company documents"
value="company-documents"
liProps={{ className: 'py-0 pe-4 group' }}
subitems={businessSubitems}
/>

<AccordionCard.Item
title="Individual's documents"
value="individual-documents"
liProps={{ className: 'py-0 pe-4 group' }}
subitems={individualsSubitems}
/>
</>
);
},
);

DocumentTrackerItems.displayName = 'DocumentTrackerItems';
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { CheckCircle } from '@ballerine/ui';
import { FilePlus2Icon } from 'lucide-react';
import { ReactNode } from 'react';

import { ClockCircle } from '@/common/components/atoms/ClockCircle/ClockCircle';
import { IndicatorCircle } from '@/common/components/atoms/IndicatorCircle/IndicatorCircle';
import { XCircle } from '@/common/components/atoms/XCircle/XCircle';
import { TDocumentsTrackerItem } from '@/domains/documents/schemas';

export const Icon = {
CHECK: (
<CheckCircle
size={18}
className={`stroke-success`}
containerProps={{
className: 'bg-success/20',
}}
/>
),
X: (
<XCircle
size={18}
className={`stroke-destructive`}
containerProps={{
className: 'bg-destructive/20',
}}
/>
),
INDICATOR: (
<IndicatorCircle
size={18}
className={`stroke-transparent`}
containerProps={{
className: 'bg-slate-500/20',
}}
/>
),
REQUESTED: (
<ClockCircle
size={18}
className={`fill-violet-500 stroke-white`}
containerProps={{
className: 'bg-violet-500/20',
}}
/>
),
MARKED: <FilePlus2Icon className="stroke-warning" size={16.5} />,
} as const;

export const documentStatusToIcon: Record<
TDocumentsTrackerItem['business'][number]['status'],
ReactNode
> = {
unprovided: Icon.INDICATOR,
provided: Icon.CHECK,
requested: Icon.REQUESTED,
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Button, ctw } from '@ballerine/ui';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { titleCase } from 'string-ts';

import { useRequestDocumentsMutation } from '@/domains/documents/hooks/mutations/useRequestDocumentsMutation/useRequestDocumentsMutation';
import { useDocumentsTrackerItemsQuery } from '@/domains/documents/hooks/queries/useDocumentsTrackerItemsQuery';
import { documentsQueryKeys } from '@/domains/documents/hooks/query-keys';
import { DocumentTrackerItemSchema, TDocumentsTrackerItem } from '@/domains/documents/schemas';
import { useCurrentCaseQuery } from '@/pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery';
import { CommonWorkflowStates } from '@ballerine/common';
import z from 'zod';
import { documentStatusToIcon, Icon } from '../constants';
import { DocumentTrackerItemOptions } from '../components/DocumentTrackerItemOptions/DocumentTrackerItemOptions';
import { X } from 'lucide-react';

export const useDocumentTracker = ({ workflowId }: { workflowId: string }) => {
const { data: documentTrackerItems } = useDocumentsTrackerItemsQuery({ workflowId });

const [open, onOpenChange] = useState(false);
const [selectedIdsToRequest, setSelectedIdsToRequest] = useState<
Array<z.infer<typeof DocumentTrackerItemSchema>['identifiers']>
>([]);

const queryClient = useQueryClient();
const { mutate: requestDocuments } = useRequestDocumentsMutation({
onSuccess: () => {
setSelectedIdsToRequest([]);
onOpenChange(false);
void queryClient.invalidateQueries(documentsQueryKeys.trackerItems({ workflowId }));
},
});
const { data: workflow } = useCurrentCaseQuery();

const onRequestDocuments = useCallback(
() =>
requestDocuments({
workflowId,
documents: selectedIdsToRequest.map(identifier => ({
type: identifier.document.type,
category: identifier.document.category,
issuingCountry: identifier.document.issuingCountry,
issuingVersion: identifier.document.issuingVersion,
decisionReason: identifier.document.decisionReason,
version: identifier.document.version,
templateId: identifier.document.type,
entity: {
id: identifier.entity.id,
type: identifier.entity.entityType,
},
})),
}),
[requestDocuments, selectedIdsToRequest, workflowId],
);

const getSubItems = useCallback(
(
documentTrackerItem:
| TDocumentsTrackerItem['business'][number]
| TDocumentsTrackerItem['individuals']['ubos'][number]
| TDocumentsTrackerItem['individuals']['directors'][number],
) => {
const { identifiers, status } = documentTrackerItem;
const compareIdentifiers = (
identifiersA: z.infer<typeof DocumentTrackerItemSchema>['identifiers'],
identifiersB: z.infer<typeof DocumentTrackerItemSchema>['identifiers'],
) => {
return [
identifiersA.document.type === identifiersB.document.type,
identifiersA.document.category === identifiersB.document.category,
identifiersA.document.issuingCountry === identifiersB.document.issuingCountry,
identifiersA.document.issuingVersion === identifiersB.document.issuingVersion,
identifiersA.document.version === identifiersB.document.version,
identifiersA.entity.id === identifiersB.entity.id,
].every(Boolean);
};

const selectedIndex = selectedIdsToRequest.findIndex(selectedIdentifiers =>
compareIdentifiers(selectedIdentifiers, identifiers),
);
const isSelected = selectedIndex > -1;

const onUnmark = () => {
setSelectedIdsToRequest(prev => prev.toSpliced(selectedIndex, 1));
};
const onMarkChange = (reason?: string) => {
if (status !== 'unprovided') {
return;
}

identifiers.document.decisionReason = reason || 'Document requested';

return setSelectedIdsToRequest(prev => [...prev, identifiers]);
};

return {
leftIcon: selectedIndex === -1 ? documentStatusToIcon[status] : Icon.MARKED,
rightIcon: !isSelected ? (
<DocumentTrackerItemOptions
isDisabled={status !== 'unprovided'}
onMarkChange={onMarkChange}
/>
) : (
<Button
variant="outline"
size="icon"
className="invisible ms-auto text-muted-foreground d-5 focus-visible:visible group-hover:visible aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:bg-background aria-disabled:opacity-50 data-[state=open]:visible"
onClick={onUnmark}
>
<X />
</Button>
),
text: (
<div className="flex flex-col space-y-0.5">
{documentTrackerItem.identifiers.entity.entityType !== 'business' && (
<span className="font-bold text-gray-900">
{[
documentTrackerItem.identifiers.entity.firstName,
documentTrackerItem.identifiers.entity.lastName,
]
.filter(Boolean)
.join(' ')}
</span>
)}
<span className="text-sm font-medium text-gray-900">
{titleCase(documentTrackerItem.identifiers.document.category ?? 'N/A')}
</span>
<span className="text-xs text-gray-500">
{titleCase(documentTrackerItem.identifiers.document.type ?? 'N/A')}
</span>
</div>
),
itemClassName: ctw('p-1', {
'bg-warning/20 rounded-md': isSelected,
}),
};
},
[selectedIdsToRequest],
);

return {
documentTrackerItems,
getSubItems,
selectedIdsToRequest,
onRequestDocuments,
open,
onOpenChange,
isRequestButtonDisabled: !workflow?.nextEvents?.some(event =>
[CommonWorkflowStates.REVISION, CommonWorkflowStates.MANUAL_REVIEW].includes(event),
),
};
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FunctionComponentWithChildren } from '@/common/types';
import { ctw } from '@/common/utils/ctw/ctw';
import { isCsv } from '@/common/utils/is-csv/is-csv';
import { isPdf } from '@/common/utils/is-pdf/is-pdf';
import { ComponentProps } from 'react';
import ReactCrop, { Crop } from 'react-image-crop';
@@ -30,7 +31,7 @@ export const ImageEditor: FunctionComponentWithChildren<IImageEditorProps> = ({
<TransformComponent
wrapperClass={`d-full max-w-[600px] max-h-[600px] h-full`}
contentClass={ctw({
'hover:cursor-move': !isPdf(image),
'hover:cursor-move': !isPdf(image) && !isCsv(image),
})}
wrapperStyle={{
width: '100%',
@@ -41,15 +42,15 @@ export const ImageEditor: FunctionComponentWithChildren<IImageEditorProps> = ({
contentStyle={{
width: '100%',
height: '100%',
display: !isPdf(image) ? 'block' : 'flex',
display: !isPdf(image) && !isCsv(image) ? 'block' : 'flex',
}}
>
<ReactCrop
crop={crop}
onChange={onCrop}
disabled={!isCropping || isPdf(image) || isRotatedOrTransformed}
disabled={!isCropping || isPdf(image) || isCsv(image) || isRotatedOrTransformed}
className={ctw('h-full w-full overflow-hidden [&>div]:!w-full', {
'flex flex-row [&>div]:min-h-[600px]': isPdf(image),
'flex flex-row [&>div]:min-h-[600px]': isPdf(image) || isCsv(image),
})}
>
<div
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ctw } from '@/common/utils/ctw/ctw';
import { ComponentProps, FunctionComponent } from 'react';
import { Loader2, ScanTextIcon } from 'lucide-react';
import { Loader2, ScanText, Sparkles } from 'lucide-react';
import { Tooltip } from '@/common/components/atoms/Tooltip/Tooltip';
import { TooltipTrigger } from '@/common/components/atoms/Tooltip/Tooltip.Trigger';
import { TooltipContent } from '@/common/components/atoms/Tooltip/Tooltip.Content';
import { TooltipProvider } from '@/common/components/atoms/Tooltip/Tooltip.Provider';

export interface IImageOCR extends ComponentProps<'button'> {
onOcrPressed?: () => void;
@@ -14,22 +18,51 @@ export const ImageOCR: FunctionComponent<IImageOCR> = ({
className,
isLoadingOCR,
...props
}) => (
<button
{...props}
type="button"
className={ctw(
'btn btn-circle btn-ghost btn-sm bg-base-300/70 text-[0.688rem] focus:outline-primary disabled:bg-base-300/70',
isLoadingOCR,
className,
)}
onClick={() => onOcrPressed?.()}
disabled={isOcrDisabled || isLoadingOCR}
>
{isLoadingOCR ? (
<Loader2 className="animate-spin stroke-foreground" />
) : (
<ScanTextIcon className={'p-0.5'} />
)}
</button>
);
}) => {
return (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
{...props}
type="button"
className={ctw(
'btn btn-circle btn-sm relative focus:outline-primary',
'transition-all duration-200',
'disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
onClick={onOcrPressed}
disabled={isOcrDisabled || isLoadingOCR}
>
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-purple-600 to-indigo-700" />

{isLoadingOCR ? (
<Loader2 className="relative z-10 animate-spin stroke-white" />
) : (
<div className="relative z-10">
<ScanText className="text-white" />
<div className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 rounded-full bg-blue-400/80" />
</div>
)}
</button>
</TooltipTrigger>
<TooltipContent
side="top"
align="center"
className="max-w-48 rounded-xl border border-indigo-200 bg-white/95 p-2 text-xs shadow-lg"
>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-indigo-700">
<Sparkles className="h-3.5 w-3.5 text-indigo-500" />
<span className="font-medium">AI Document Recognition</span>
</div>
<p className="max-w-full text-gray-600">
Our AI can detect documents and prefill data for faster verification.
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ export const OverallRiskLevel: FunctionComponent<{
className={ctw(
{
[severityToTextClassName[
(severity?.toUpperCase() as keyof typeof severityToClassName) ?? 'DEFAULT'
(severity as keyof typeof severityToClassName) ?? 'DEFAULT'
]]: riskScore || riskScore === 0,
},
{
@@ -45,14 +45,14 @@ export const OverallRiskLevel: FunctionComponent<{
)}
checkFalsy={false}
>
{riskScore}
{typeof riskScore === 'number' && !Number.isNaN(riskScore)
? Math.min(riskScore, 100)
: null}
</TextWithNAFallback>
{(riskScore || riskScore === 0) && (
<Badge
className={ctw(
severityToClassName[
(severity?.toUpperCase() as keyof typeof severityToClassName) ?? 'DEFAULT'
],
severityToClassName[(severity as keyof typeof severityToClassName) ?? 'DEFAULT'],
{
'text-background': severity === Severity.CRITICAL,
},
Original file line number Diff line number Diff line change
@@ -99,5 +99,8 @@ export const pluginsWhiteList = [
'companySanctions',
'merchantMonitoring',
'merchantScreening',
'bankAccountVerification',
'commercialCreditCheck',
] as const;

export const DEFAULT_PROCESS_TRACKER_PROCESSES = ['collection-flow', 'third-party', 'ubos'];
Original file line number Diff line number Diff line change
@@ -3,18 +3,19 @@ import { FunctionComponent } from 'react';

export const Search: FunctionComponent<{
value: string;
placeholder?: string;
onChange: (search: string) => void;
}> = ({ value, onChange }) => {
}> = ({ value, placeholder, onChange }) => {
return (
<div className="relative flex flex-col gap-1">
<div className="input-group flex h-[32px] items-center rounded-[44px] border border-[#E5E7EB] shadow-[0_4px_4px_0_rgba(174,174,174,0.0625)]">
<div className="input-group flex h-8 w-48 items-center rounded-[44px] border border-[#E5E7EB] shadow-[0_4px_4px_0_rgba(174,174,174,0.0625)]">
<div className={`btn btn-square btn-ghost pointer-events-none -ms-2`}>
<LucideSearch size={13} />
</div>
<input
type={'search'}
className="input input-xs -ml-3 h-[18px] w-full !border-0 pl-0 text-xs !outline-none !ring-0 placeholder:text-base-content"
placeholder={`Search`}
placeholder={placeholder ?? `Search`}
value={value}
onChange={e => onChange(e.target.value)}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { DialogClose } from '@radix-ui/react-dialog';
import { CircleCheck } from 'lucide-react';
import { Link } from 'react-router-dom';

import { useLocale } from '@/common/hooks/useLocale/useLocale';
import { Button } from '@/common/components/atoms/Button/Button';
import { Separator } from '@/common/components/atoms/Separator/Separator';
import { Dialog } from '@/common/components/organisms/Dialog/Dialog';
import { DialogContent } from '@/common/components/organisms/Dialog/Dialog.Content';
import { DialogDescription } from '@/common/components/organisms/Dialog/Dialog.Description';
import { DialogFooter } from '@/common/components/organisms/Dialog/Dialog.Footer';
import { DialogHeader } from '@/common/components/organisms/Dialog/Dialog.Header';
import { DialogTitle } from '@/common/components/organisms/Dialog/Dialog.Title';
import { useToggle } from '@/common/hooks/useToggle/useToggle';
import { BusinessReportsLeftCard } from '@/domains/business-reports/components/BusinessReportsLeftCard/BusinessReportsLeftCard';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { Skeleton } from '@ballerine/ui';

const benefits = [
'Spot potential risks and violations',
'Assess legitimacy and card scheme compliance',
'Gain actionable insights instantly',
];

export const WelcomeModal = () => {
const [open, toggleOpen] = useToggle(true);
const { data: customer, isLoading: isLoadingCustomer } = useCustomerQuery();
const locale = useLocale();

if (isLoadingCustomer || customer?.config?.demoAccessDetails?.seenWelcomeModal !== false) {
return null;
}

const { reportsLeft, demoDaysLeft } = customer?.config?.demoAccessDetails ?? {};

return (
<Dialog open={open} onOpenChange={toggleOpen}>
<DialogContent className="px-0 sm:max-w-xl">
<DialogHeader className="items-center px-6">
<DialogTitle className={`text-2xl`}>Welcome to Ballerine</DialogTitle>
<DialogDescription className={`text-md`}>
Welcome to Ballerine’s Web Presence Free Trial! 🚀
</DialogDescription>
</DialogHeader>
<div className="px-6">
<div style={{ position: 'relative', paddingBottom: '56.25%', height: 0 }}>
<Skeleton className="size-full absolute inset-0" />
<iframe
src="https://www.loom.com/embed/7cd69b5e2db24e81ace760cc38b3d7dc?sid=69a0ffbf-bd57-4e88-b9db-cbf819da21d3"
frameBorder="0"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
/>
</div>
</div>

<BusinessReportsLeftCard
reportsLeft={reportsLeft}
demoDaysLeft={demoDaysLeft}
className="mx-6"
/>

<Separator />

<div className="space-y-4 px-6">
<p className="font-bold">Analyze any merchant&apos;s online presence</p>

{benefits.map(benefit => (
<div key={benefit} className="flex items-center gap-2">
<CircleCheck className="text-success d-5" />
<p>{benefit}</p>
</div>
))}
</div>

<Separator />

<DialogFooter className="px-6">
<DialogClose asChild>
<Link to={`${locale}/merchant-monitoring`}>
<Button className="text-md rounded-lg font-bold">Get Started</Button>
</Link>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -3,62 +3,78 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import { ctw } from '@/common/utils/ctw/ctw';
import { DayPicker, DayPickerRangeProps } from 'react-day-picker';
import { buttonVariants } from '../../atoms/Button/Button';
import { Button } from '@ballerine/ui';

export type CalendarProps = DayPickerRangeProps;

export const Calendar = ({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) => {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={ctw('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: ctw(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: ctw(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md',
),
day: ctw(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
<div className={`flex flex-col`}>
<DayPicker
showOutsideDays={showOutsideDays}
className={ctw('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: ctw(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: ctw(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md',
),
day: ctw(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
<div className={`flex w-full justify-end`}>
<Button
variant={`ghost`}
className={ctw(`!mt-0 h-8 select-none font-normal hover:bg-transparent`, {
'pointer-events-none opacity-50': !(props.selected?.from && props.selected?.to),
})}
onClick={e => props.onSelect?.({ from: undefined, to: undefined }, new Date(), {}, e)}
>
Clear dates
</Button>
</div>
</div>
);
};

Calendar.displayName = 'Calendar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ReactNode } from 'react';

import {
ExperienceBallerineCard,
ExperienceBallerineCardProps,
} from '@/common/components/molecules/DemoAccessCards/ExperienceBallerineCard';
import { GetFullAccessCard } from '@/common/components/molecules/DemoAccessCards/GetFullAccessCard';
import { Separator } from '@/common/components/atoms/Separator/Separator';
import { env } from '@/common/env/env';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { ctw } from '@/common/utils/ctw/ctw';

export type DemoAccessWrapperProps = {
children: ReactNode;
} & Omit<ExperienceBallerineCardProps, 'className'>;
export const DemoAccessWrapper = ({ children, ...props }: DemoAccessWrapperProps) => {
const { data: customer } = useCustomerQuery();

return (
<div className={ctw('space-y-10', { 'pt-6': !customer?.config?.isDemoAccount })}>
{customer?.config?.isDemoAccount && (
<>
<div className="flex flex-col gap-4 px-6 pt-6 xl:flex-row">
<ExperienceBallerineCard {...props} className="w-full xl:w-1/2" />
<GetFullAccessCard className="w-full xl:w-1/2" />
</div>

<Separator />
</>
)}

{children}
</div>
);
};

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCsv } from '@/common/utils/is-csv/is-csv';
import { forwardRef, useCallback, useEffect, useState } from 'react';
import { ctw } from '../../../utils/ctw/ctw';
import { isPdf } from '../../../utils/is-pdf/is-pdf';
@@ -31,10 +32,10 @@ export const SelectedImage = forwardRef<HTMLImageElement | HTMLIFrameElement, TS
setIsError(false);
}, [isError, selectedImage?.imageUrl]);

if (isPdf(selectedImage)) {
if (isPdf(selectedImage) || isCsv(selectedImage)) {
return (
<iframe
src={selectedImage?.imageUrl + '#toolbar=0&navpanes=0'}
src={`${selectedImage?.imageUrl}#toolbar=0&navpanes=0`}
ref={ref}
className={ctw(className, `d-full mx-auto`, {
'h-[600px] w-[600px]': isPlaceholder,
@@ -60,3 +61,5 @@ export const SelectedImage = forwardRef<HTMLImageElement | HTMLIFrameElement, TS
);
},
);

SelectedImage.displayName = 'SelectedImage';
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isCsv } from '@/common/utils/is-csv/is-csv';
import { FunctionComponent } from 'react';
import { useImageViewerContext } from './hooks/useImageViewerContext/useImageViewerContext';
import { IZoomModalProps } from './interfaces';
import { Modal } from '../Modal/Modal';
import { BallerineImage } from '../../atoms/BallerineImage';
import { ctw } from '../../../utils/ctw/ctw';
import { isPdf } from '../../../utils/is-pdf/is-pdf';
import { BallerineImage } from '../../atoms/BallerineImage';
import { Modal } from '../Modal/Modal';
import { useImageViewerContext } from './hooks/useImageViewerContext/useImageViewerContext';
import { IZoomModalProps } from './interfaces';

/**
* @description To be used by {@link ImageViewer}. Uses the {@link Modal} component with default styling to display an enlarged version of the selected image.
@@ -33,14 +34,14 @@ export const ZoomModal: FunctionComponent<IZoomModalProps> = ({
hideTitle
{...rest}
>
{isPdf(selectedImage) && (
{(isPdf(selectedImage) || isCsv(selectedImage)) && (
<iframe
src={selectedImage?.imageUrl}
src={`${selectedImage?.imageUrl}${isCsv(selectedImage) ? '#toolbar=0&navpanes=0' : ''}`}
className={ctw(`d-full`, imageClassName)}
{...restImage}
/>
)}
{!isPdf(selectedImage) && (
{!isPdf(selectedImage) && !isCsv(selectedImage) && (
<BallerineImage
withPlaceholder
src={selectedImage?.imageUrl}
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.Component
ref={ref}
data-sidebar="menu-badge"
className={ctw(
'min-w-5 text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums',
'text-sidebar-foreground min-w-5 pointer-events-none absolute right-1 flex h-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ export const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={ctw(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground [&>svg]:size-4 flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
Original file line number Diff line number Diff line change
@@ -5,15 +5,21 @@ import { useIsMobile } from '@/common/components/organisms/Sidebar/hooks/useIsMo
import { TSidebarContext } from './types';
import { SidebarContext } from './Sidebar.Context';

const SIDEBAR_WIDTH = '25rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_WIDTH = '20rem';
const SIDEBAR_WIDTH_XL = '25rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;

export const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
style?: React.CSSProperties & {
'--sidebar-width'?: string;
'--sidebar-width-mobile'?: string;
'--sidebar-width-xl'?: string;
};
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
@@ -96,13 +102,14 @@ export const SidebarProvider = React.forwardRef<
<div
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-xl': SIDEBAR_WIDTH_XL,
...style,
} as React.CSSProperties
}
className={ctw(
'group/sidebar-wrapper min-h-svh has-[[data-variant=inset]]:bg-sidebar flex w-full',
'group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar min-h-svh flex w-full',
className,
)}
ref={ref}
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ const Sidebar = React.forwardRef<
return (
<div
className={ctw(
'bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col',
'bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col xl:w-[--sidebar-width-xl]',
className,
)}
ref={ref}
@@ -71,7 +71,7 @@ const Sidebar = React.forwardRef<
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 xl:w-[--sidebar-width-xl] [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
@@ -97,7 +97,7 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */}
<div
className={ctw(
'h-svh relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'h-svh relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear 2xl:w-[--sidebar-width-xl]',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
@@ -107,7 +107,7 @@ const Sidebar = React.forwardRef<
/>
<div
className={ctw(
'h-svh fixed inset-y-0 z-10 hidden w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
'h-svh fixed inset-y-0 z-10 hidden w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex 2xl:w-[--sidebar-width-xl]',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { ComponentProps, FunctionComponent } from 'react';
import { useSort } from '@/common/hooks/useSort/useSort';
import { useSelect } from '@/common/hooks/useSelect/useSelect';
import { DataTable } from '@ballerine/ui';
import { ComponentProps, FunctionComponent } from 'react';
import { PartialDeep } from 'type-fest';

import { usePersistentScroll } from '@/common/hooks/usePersistentScroll/usePersistentScroll';
import { useSelect } from '@/common/hooks/useSelect/useSelect';
import { useSort } from '@/common/hooks/useSort/useSort';

export const UrlDataTable: FunctionComponent<
Omit<ComponentProps<typeof DataTable>, 'sort' | 'select'> &
PartialDeep<Pick<ComponentProps<typeof DataTable>, 'sort' | 'select'>>
> = props => {
const { sortDir, sortBy, onSort } = useSort();
const { selected, onSelect } = useSelect();
const { ref, handleScroll } = usePersistentScroll();

return (
<DataTable
{...props}
ref={ref}
handleScroll={handleScroll}
sort={{
sortBy,
sortDir,
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { FunctionComponent, PropsWithChildren } from 'react';
import { TooltipProvider } from '@ballerine/ui';
import { PostHogProvider } from 'posthog-js/react';

import { queryClient } from '@/lib/react-query/query-client';
import { AuthProvider } from '@/domains/auth/context/AuthProvider/AuthProvider';
import { env } from '@/common/env/env';

export const Providers: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
<PostHogProvider
apiKey={env.VITE_POSTHOG_KEY}
options={{
api_host: env.VITE_POSTHOG_HOST,
person_profiles: 'identified_only',
loaded: ph => {
ph.register_for_session({ environment: env.VITE_ENVIRONMENT_NAME });
},
}}
>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<TooltipProvider delayDuration={100}>{children}</TooltipProvider>
</AuthProvider>
</QueryClientProvider>
</PostHogProvider>
);
};
10 changes: 8 additions & 2 deletions apps/backoffice-v2/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
export const DOWNLOAD_ONLY_MIME_TYPES = [
'application/csv',
'text/csv',
// xls
'application/vnd.ms-excel',
// xlsx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
] as const;

export const BALLERINE_CALENDLY_LINK = 'https://calendly.com/d/cp53-ryw-4s3/ballerine-intro';

export const URL_REGEX =
/((https?):\/\/)?([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9]+(\.[a-z]{2})?(\/[a-zA-Z0-9_#-]+)*(\/)?(\?[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*)?(#[a-zA-Z0-9_-]+)?/;

export const NO_VIOLATION_DETECTED_RISK_INDICATOR_ID = 'no-violation-detected';
export const POSITIVE_RISK_LEVEL_ID = 'positive';
4 changes: 3 additions & 1 deletion apps/backoffice-v2/src/common/env/env.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,9 @@ export const formatErrors = (errors: ZodFormattedError<Map<string, string>, stri
.filter(Boolean);
};

const _env = EnvSchema.safeParse(import.meta.env);
const envSource = (globalThis as any).env?.VITE_API_URL ? (globalThis as any).env : import.meta.env;

const _env = EnvSchema.safeParse(envSource);

// TypeScript complains with !env.success
if (_env.success === false) {
37 changes: 24 additions & 13 deletions apps/backoffice-v2/src/common/env/schema.ts
Original file line number Diff line number Diff line change
@@ -5,14 +5,22 @@ export const EnvSchema = z.object({
VITE_ENVIRONMENT_NAME: z.enum(['development', 'production', 'sandbox', 'local']),
VITE_API_URL: z.string().url().default('https://api-dev.ballerine.io/v2'),
VITE_API_KEY: z.string(),
VITE_AUTH_ENABLED: z.preprocess(
value => (typeof value === 'string' ? JSON.parse(value) : value),
z.boolean().default(true),
),
VITE_MOCK_SERVER: z.preprocess(
value => (typeof value === 'string' ? JSON.parse(value) : value),
z.boolean().default(true),
),
VITE_AUTH_ENABLED: z.preprocess(value => {
try {
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (error) {
console.warn('Failed to parse VITE_AUTH_ENABLED, defaulting to true', error);
return true;
}
}, z.boolean().default(true)),
VITE_MOCK_SERVER: z.preprocess(value => {
try {
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (error) {
console.warn('Failed to parse VITE_MOCK_SERVER, defaulting to true', error);
return true;
}
}, z.boolean().default(true)),
VITE_POLLING_INTERVAL: z.coerce
.number()
.transform(v => v * 1000)
@@ -24,10 +32,14 @@ export const EnvSchema = z.object({
.or(z.literal(false))
.catch(undefined),
VITE_IMAGE_LOGO_URL: z.string().optional(),
VITE_FETCH_SIGNED_URL: z.preprocess(
value => (typeof value === 'string' ? JSON.parse(value) : value),
z.boolean().default(true),
),
VITE_FETCH_SIGNED_URL: z.preprocess(value => {
try {
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (error) {
console.warn('Failed to parse VITE_FETCH_SIGNED_URL, defaulting to true', error);
return true;
}
}, z.boolean().default(true)),
VITE_POSTHOG_KEY: z.string().optional(),
VITE_POSTHOG_HOST: z.string().optional(),
VITE_SENTRY_DSN: z.string().optional(),
@@ -38,6 +50,5 @@ export const EnvSchema = z.object({

return new RegExp(value);
}, z.custom<RegExp>(value => value instanceof RegExp).optional()),
VITE_SAOLA_API_KEY: z.string().optional(),
VITE_BOTPRESS_CLIENT_ID: z.string().default('8f29c89d-ec0e-494d-b18d-6c3590b28be6'),
});
26 changes: 6 additions & 20 deletions apps/backoffice-v2/src/common/hooks/useHomeLogic/useHomeLogic.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { ComponentProps, useEffect } from 'react';
import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker';
import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams';
import { HomeSearchSchema } from '@/pages/Home/home-search-schema';
import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery';
import { useLocale } from '@/common/hooks/useLocale/useLocale';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

export const useHomeLogic = () => {
const locale = useLocale();
const { pathname, search } = useLocation();
const navigate = useNavigate();
const [{ from, to }, setSearchParams] = useZodSearchParams(HomeSearchSchema);
const { data: session } = useAuthenticatedUserQuery();
const { data: customer, isLoading: isLoadingCustomer } = useCustomerQuery();
const isExample = customer?.config?.isExample;
const isDemo = customer?.config?.isDemo;
const isMerchantMonitoringEnabled = customer?.config?.isMerchantMonitoringEnabled;
const { firstName, fullName, avatarUrl } = session?.user || {};
const statisticsLink = `/${locale}/home/statistics${search}`;
const workflowsLink = `/${locale}/home/workflows${search}`;
@@ -26,28 +22,18 @@ export const useHomeLogic = () => {
return;
}

navigate(`/${locale}/home/statistics`);
navigate(`/${locale}/home/statistics`, { replace: true });
}, [pathname, locale, navigate]);

const onDateRangeChange: ComponentProps<typeof DateRangePicker>['onChange'] = range => {
const from = range?.from?.toISOString();
const to = range?.to?.toISOString();

setSearchParams({ from, to });
};

return {
from,
to,
firstName,
fullName,
avatarUrl,
statisticsLink,
workflowsLink,
defaultTabValue,
onDateRangeChange,
isLoadingCustomer,
isExample,
isDemo,
isMerchantMonitoringEnabled,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect, useState } from 'react';

export const useMobileBreakpoint = () => {
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);

return () => window.removeEventListener('resize', checkMobile);
}, []);

return { isMobile };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect, useRef } from 'react';

export const usePersistentScroll = () => {
const scrollAreaRef = useRef<HTMLDivElement>(null);

const resetScrollPosition = () => {
sessionStorage.removeItem('scrollPosition');
};

const restoreScrollPosition = () => {
const savedPosition = sessionStorage.getItem('scrollPosition');

if (savedPosition && scrollAreaRef.current) {
scrollAreaRef.current.scroll(0, parseInt(savedPosition, 10));
}
};

useEffect(() => {
if (scrollAreaRef.current?.scrollTop === 0) {
return restoreScrollPosition();
}
}, []);

const handleScroll = () => {
const scrollTop = scrollAreaRef.current?.scrollTop ?? 0;
sessionStorage.setItem('scrollPosition', scrollTop.toString());
};

return { ref: scrollAreaRef, handleScroll };
};
15 changes: 4 additions & 11 deletions apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx
Original file line number Diff line number Diff line change
@@ -3,16 +3,8 @@ import { useDebounce } from '../useDebounce/useDebounce';
import { useSerializedSearchParams } from '@/common/hooks/useSerializedSearchParams/useSerializedSearchParams';
import { useIsMounted } from '@/common/hooks/useIsMounted/useIsMounted';

export const useSearch = (
{
initialSearch = '',
}: {
initialSearch?: string;
} = {
initialSearch: '',
},
) => {
const [{ search = initialSearch }, setSearchParams] = useSerializedSearchParams();
export const useSearch = () => {
const [{ search }, setSearchParams] = useSerializedSearchParams();
const [_search, setSearch] = useState(search);
const debouncedSearch = useDebounce(_search, 240);
const onSearchChange = useCallback((search: string) => {
@@ -32,7 +24,8 @@ export const useSearch = (
}, [debouncedSearch]);

return {
search: _search,
search: _search as string,
debouncedSearch: debouncedSearch as string,
onSearch: onSearchChange,
};
};
Original file line number Diff line number Diff line change
@@ -39,10 +39,10 @@ export const MonitoringReportsTabs = [

export const CaseTabs = [
'summary',
'companyInformation',
'kyb',
'storeInfo',
'documents',
'ubos',
'ubosKyc',
'associatedCompanies',
'directors',
'monitoringReports',
@@ -51,13 +51,13 @@ export const CaseTabs = [

export const TabToLabel = {
summary: 'Summary',
companyInformation: 'Company',
storeInformation: 'Store',
kyb: 'KYB',
storeInfo: 'Store',
documents: 'Documents',
ubos: 'UBOs',
ubosKyc: 'KYC',
associatedCompanies: 'Associated Companies',
directors: 'Directors',
monitoringReports: 'Monitoring Reports',
monitoringReports: 'Web Presence',
customData: 'Custom Data',
} as const;

Original file line number Diff line number Diff line change
@@ -5,9 +5,12 @@ import { defaultSerializer } from '@/common/hooks/useZodSearchParams/utils/defau
import { ISerializedSearchParams } from '@/common/hooks/useZodSearchParams/interfaces';

export const useSerializedSearchParams = (options: ISerializedSearchParams = {}) => {
const { search, pathname, state } = useLocation();
const serializer = options.serializer ?? defaultSerializer;
const deserializer = options.deserializer ?? defaultDeserializer;
const { search, pathname, state, hash } = useLocation();
const {
serializer = defaultSerializer,
deserializer = defaultDeserializer,
replace = false,
} = options;
const searchParamsAsObject = useMemo(() => deserializer(search), [deserializer, search]);
const navigate = useNavigate();

@@ -17,13 +20,11 @@ export const useSerializedSearchParams = (options: ISerializedSearchParams = {})
`${pathname}${serializer({
...searchParamsAsObject,
...searchParams,
})}`,
{
state,
},
})}${hash}`,
{ state, replace },
);
},
[navigate, pathname, searchParamsAsObject, serializer, state],
[navigate, pathname, searchParamsAsObject, serializer, state, hash],
);

return [searchParamsAsObject, setSearchParams] as const;
Original file line number Diff line number Diff line change
@@ -3,4 +3,5 @@ import qs from 'qs';
export interface ISerializedSearchParams {
serializer?: (searchParams: Record<string, unknown>) => string;
deserializer?: (searchParams: string) => qs.ParsedQs;
replace?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { jsPDF } from 'jspdf';
import 'jspdf-autotable';
import Papa from 'papaparse';

interface jsPDFWithPlugin extends jsPDF {
autoTable: any;
}

export const convertCsvToPdfBase64String = (csvBase64: string) => {
// Extract base64 data from data URI
const base64Data = csvBase64.split(',')[1] || csvBase64;

// Decode base64 to string
const csvString = atob(base64Data);

// Parse CSV string to array using PapaParse
const { data } = Papa.parse(csvString, {
header: true,
skipEmptyLines: true,
});

// Create new PDF document
const doc = new jsPDF() as jsPDFWithPlugin;

// Add table to PDF using autoTable
doc.autoTable({
head: [Object.keys(data[0] as object)], // Column headers
body: data.map(row => Object.values(row as object)), // Row data
startY: 10,
margin: { top: 10 },
styles: { fontSize: 8 },
headStyles: { fillColor: [66, 66, 66] },
});

return doc.output('datauristring');
};
99 changes: 99 additions & 0 deletions apps/backoffice-v2/src/common/utils/export-to-csv/export-to-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import dayjs from 'dayjs';

/**
* Formats a value for CSV export
* - Handles Date objects by converting to ISO format in local timezone
* - Handles arrays by joining with semicolons
* - Handles objects by converting to JSON strings
* - Handles primitive values directly
*
* @param value - The value to format
* @returns Formatted string value
*/
const formatValueForCsv = (value: unknown): string => {
if (value === null || value === undefined) {
return '';
}

// Handle Date objects - convert to ISO format in local timezone
if (value instanceof Date) {
return dayjs(value).format('YYYY-MM-DDTHH:mm:ssZ');
}

// Handle arrays - join with semicolons
if (Array.isArray(value)) {
return value.map(item => formatValueForCsv(item)).join(',\n');
}

// Handle objects - convert to JSON
if (typeof value === 'object') {
return Object.entries(value)
.map(([key, val]) => `${key}: ${formatValueForCsv(val)}`)
.join(',\n');
}

// Handle primitive values
return String(value).replace(/"/g, '""');
};

/**
* Converts an array of objects to a CSV string
* @param data Array of objects to convert to CSV
* @returns CSV string
*/
export const convertToCSV = (data: Array<Record<string, unknown>>) => {
if (!data || data.length === 0) {
return '';
}

// Extract column headers from the first item
const headers = Object.keys(data[0] || {});
const csvRows = [headers.join(',')];

for (const item of data) {
const values = headers.map(header => {
const value = item[header];
const formattedValue = formatValueForCsv(value);

// Wrap in quotes if contains commas, quotes, or newlines
return formattedValue.includes(',') ||
formattedValue.includes('"') ||
formattedValue.includes('\n')
? `"${formattedValue}"`
: formattedValue;
});
csvRows.push(values.join(','));
}

return csvRows.join('\n');
};

/**
* Exports data to a CSV file and triggers a download
* @param data Array of objects to export
* @param filename Name of the file to download (without extension)
*/
export const exportToCSV = (data: Array<Record<string, unknown>>, filename: string): boolean => {
if (!data || data.length === 0) {
return false;
}

// Generate CSV and trigger download
const csv = convertToCSV(data);
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');

link.href = url;
const fullFilename = `${filename}.csv`;
link.setAttribute('download', fullFilename);

document.body.appendChild(link);
link.click();
document.body.removeChild(link);

// Clean up the URL object
URL.revokeObjectURL(url);

return true;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { expect, describe, test, vi, beforeEach, afterEach } from 'vitest';
import { convertToCSV, exportToCSV } from './export-to-csv';

// Mock dayjs globally for all tests
vi.mock('dayjs', () => {
return {
default: () => ({
format: () => '2023-01-01T12:00:00+0000',
}),
};
});

describe('CSV Export Utils', () => {
describe('convertToCSV', () => {
test('should return an empty string for empty data', () => {
expect(convertToCSV([])).toBe('');
expect(convertToCSV(null as any)).toBe('');
expect(convertToCSV(undefined as any)).toBe('');
});

test('should correctly convert simple objects to CSV', () => {
const data = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 },
];
const expected = 'name,age\nJohn,30\nJane,25';
expect(convertToCSV(data)).toBe(expected);
});

test('should handle arrays in values', () => {
const data = [
{ name: 'John', hobbies: ['reading', 'swimming'] },
{ name: 'Jane', hobbies: ['hiking', 'music'] },
];

// Get actual output to adjust expectations if needed
const actual = convertToCSV(data);

// Verify it contains the correct data without being strict about quotes
expect(actual).toContain('name,hobbies');
expect(actual).toContain('John');
expect(actual).toContain('"reading,\nswimming"');
expect(actual).toContain('Jane');
expect(actual).toContain('"hiking,\nmusic"');
});

test('should handle objects in values', () => {
const data = [
{ name: 'John', details: { city: 'New York', country: 'USA' } },
{ name: 'Jane', details: { city: 'London', country: 'UK' } },
];

const actual = convertToCSV(data);
expect(actual).toEqual(
'name,details\nJohn,"city: New York,\ncountry: USA"\nJane,"city: London,\ncountry: UK"',
);
});

test('should handle Date objects', () => {
// Create a fixed date for testing
const date = new Date('2023-01-01T12:00:00Z');

const data = [{ name: 'John', created: date }];

const actual = convertToCSV(data);

// Verify date formatting without being strict about exact format
expect(actual).toContain('name,created');
expect(actual).toContain('John');
// Check just the date part as the exact format might vary
expect(actual).toContain('2023-01-01');
});

test('should handle null and undefined values', () => {
const data = [
{ name: 'John', age: null, city: undefined },
{ name: 'Jane', age: 25, city: 'London' },
];
const expected = 'name,age,city\nJohn,,\nJane,25,London';
expect(convertToCSV(data)).toBe(expected);
});

test('should properly escape values with commas and quotes', () => {
const data = [
{ name: 'John, Doe', comment: 'He said: "Hello"' },
{ name: 'Jane Doe', comment: 'Normal text' },
];

const actual = convertToCSV(data);

// Check for proper escaping without exact string matching
expect(actual).toContain('name,comment');
expect(actual).toMatch(/John, Doe/);
expect(actual).toMatch(/He said:.*Hello/);
expect(actual).toContain('Jane Doe');
expect(actual).toContain('Normal text');
});
});

describe('exportToCSV', () => {
// Setup for DOM mocking
let originalCreateObjectURL: typeof URL.createObjectURL;
let originalRevokeObjectURL: typeof URL.revokeObjectURL;
let mockLink: { href: string; setAttribute: Function; click: Function };

beforeEach(() => {
// Save original methods
originalCreateObjectURL = URL.createObjectURL;
originalRevokeObjectURL = URL.revokeObjectURL;

// Mock URL methods
URL.createObjectURL = vi.fn().mockReturnValue('blob:mock-url');
URL.revokeObjectURL = vi.fn();

// Mock link element
mockLink = {
href: '',
setAttribute: vi.fn(),
click: vi.fn(),
};

// Mock DOM methods
document.createElement = vi.fn().mockReturnValue(mockLink as unknown as HTMLElement);
document.body.appendChild = vi.fn();
document.body.removeChild = vi.fn();
});

afterEach(() => {
// Restore original methods
URL.createObjectURL = originalCreateObjectURL;
URL.revokeObjectURL = originalRevokeObjectURL;

// Clear mocks
vi.clearAllMocks();
});

test('should return false for empty data', () => {
const result = exportToCSV([], 'test-file');

expect(result).toBe(false);
});

test('should create a CSV blob and trigger download', () => {
const data = [{ name: 'John', age: 30 }];

const result = exportToCSV(data, 'test-file');

// Verify blob creation and download
expect(URL.createObjectURL).toHaveBeenCalled();
expect(document.createElement).toHaveBeenCalledWith('a');
expect(mockLink.setAttribute).toHaveBeenCalledWith(
'download',
expect.stringContaining('.csv'),
);
expect(mockLink.click).toHaveBeenCalled();
expect(document.body.appendChild).toHaveBeenCalledWith(mockLink as unknown as HTMLElement);
expect(document.body.removeChild).toHaveBeenCalledWith(mockLink as unknown as HTMLElement);
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url');

expect(result).toBe(true);
});
});
});
1 change: 1 addition & 0 deletions apps/backoffice-v2/src/common/utils/export-to-csv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './export-to-csv';
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const DEFAULT_PAGE_SIZE = 50;

/**
* A generic type for paginated API responses
*/
export interface PaginatedResponse<T> {
data: T[];
totalPages: number;
totalItems?: number;
}

/**
* A generic type for pagination parameters
*/
export interface PaginationParams {
page: {
number: number;
size: number;
};
}

/**
* Fetches all pages from a paginated API endpoint
*
* @param fetchFunction - The function to use for fetching a single page
* @param baseParams - The base parameters to pass to the fetch function (excluding pagination)
* @param pageSize - The number of items per page (default: 50)
* @param onProgress - Optional callback for progress updates
* @returns All data from all pages combined in correct page order
*/
export const fetchAllPages = async <T, P extends PaginationParams>(
fetchFunction: (params: P) => Promise<PaginatedResponse<T> | undefined>,
baseParams: Omit<P, keyof PaginationParams>,
pageSize = DEFAULT_PAGE_SIZE,
onProgress?: (current: number, total: number, items: number) => void,
): Promise<T[]> => {
try {
// Create params for the first page
const firstPageParams = {
...(baseParams as any),
page: { number: 1, size: pageSize },
} as P;

// Fetch the first page to get total pages
const firstPageResult = await fetchFunction(firstPageParams);

if (!firstPageResult) {
throw new Error('Failed to fetch data');
}

const { totalPages, totalItems = 0 } = firstPageResult;

// Start with the first page data
let allData = [...firstPageResult.data];

// If we have more than one page, fetch the rest concurrently
if (totalPages > 1) {
onProgress?.(1, totalPages, totalItems);

// Create an array of just the promises
const pagePromises: Array<Promise<PaginatedResponse<T> | undefined>> = [];

for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
const pageParams = {
...(baseParams as any),
page: { number: pageNum, size: pageSize },
} as P;

pagePromises.push(fetchFunction(pageParams));
}

// Wait for all promises to complete
const results = await Promise.all(pagePromises);

// Add each page's data - results already in correct order
results.forEach((result, index) => {
if (result?.data) {
allData = [...allData, ...result.data];
onProgress?.(index + 2, totalPages, totalItems); // pageNum = index + 2
}
});

// Final progress update
onProgress?.(totalPages, totalPages, totalItems);
}

return allData;
} catch (error) {
console.error('Error fetching all pages:', error);
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { fetchAllPages, type PaginatedResponse, type PaginationParams } from './fetch-all-pages';

// Mock data for testing
interface TestItem {
id: number;
name: string;
}

interface TestParams extends PaginationParams {
filter?: string;
sort?: string;
}

describe('fetchAllPages', () => {
// Reset all mocks before each test
beforeEach(() => {
vi.resetAllMocks();
});

test('should return data from a single page', async () => {
// Mock data for a single page response
const mockData: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

// Mock fetch function that returns a single page
const mockFetch = vi.fn().mockResolvedValue({
data: mockData,
totalPages: 1,
totalItems: mockData.length,
} as PaginatedResponse<TestItem>);

// Base parameters for the fetch
const baseParams = { filter: 'test', sort: 'asc' };

// Call the function
const result = await fetchAllPages<TestItem, TestParams>(mockFetch, baseParams, 10);

// Verify the fetch was called correctly
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith({
...baseParams,
page: { number: 1, size: 10 },
});

// Verify the result contains all the data
expect(result).toEqual(mockData);
});

test('should fetch and combine multiple pages in correct order', async () => {
// Mock data for multiple pages
const page1Data: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

const page2Data: TestItem[] = [
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
];

const page3Data: TestItem[] = [
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' },
];

// Mock fetch function that handles multiple pages
const mockFetch = vi.fn().mockImplementation((params: TestParams) => {
const pageNumber = params.page.number;

if (pageNumber === 1) {
return Promise.resolve({
data: page1Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
} else if (pageNumber === 2) {
return Promise.resolve({
data: page2Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
} else if (pageNumber === 3) {
return Promise.resolve({
data: page3Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
}

return Promise.resolve(undefined);
});

// Call the function
const result = await fetchAllPages<TestItem, TestParams>(mockFetch, { filter: 'test' }, 2);

// Verify the fetch was called for all pages
expect(mockFetch).toHaveBeenCalledTimes(3);
expect(mockFetch).toHaveBeenCalledWith({
filter: 'test',
page: { number: 1, size: 2 },
});
expect(mockFetch).toHaveBeenCalledWith({
filter: 'test',
page: { number: 2, size: 2 },
});
expect(mockFetch).toHaveBeenCalledWith({
filter: 'test',
page: { number: 3, size: 2 },
});

// Verify the result contains all the data in correct order
expect(result).toEqual([...page1Data, ...page2Data, ...page3Data]);
});

test('should maintain correct page order even if responses arrive out of order', async () => {
// Mock data for multiple pages
const page1Data: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];
const page2Data: TestItem[] = [
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
];
const page3Data: TestItem[] = [
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' },
];

// Mock fetch function that returns page 2 before page 3
const mockFetch = vi.fn().mockImplementation((params: TestParams) => {
const pageNumber = params.page.number;

if (pageNumber === 1) {
return Promise.resolve({
data: page1Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
} else if (pageNumber === 2) {
// Add a delay to page 2 to ensure it resolves after page 3
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: page2Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
}, 50);
});
} else if (pageNumber === 3) {
return Promise.resolve({
data: page3Data,
totalPages: 3,
totalItems: 6,
} as PaginatedResponse<TestItem>);
}

return Promise.resolve(undefined);
});

// Call the function
const result = await fetchAllPages<TestItem, TestParams>(mockFetch, {}, 2);

// Verify the fetch was called for all pages
expect(mockFetch).toHaveBeenCalledTimes(3);

// Verify the result contains all the data in correct order
// (even though page 3 response comes back before page 2)
expect(result).toEqual([...page1Data, ...page2Data, ...page3Data]);
});

test('should call onProgress with the correct progress information', async () => {
// Mock data
const mockData = Array.from({ length: 3 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
}));

// Mock fetch function
const mockFetch = vi.fn().mockResolvedValue({
data: mockData,
totalPages: 3,
totalItems: 9,
} as PaginatedResponse<TestItem>);

// Mock progress callback
const mockProgress = vi.fn();

// Call the function
await fetchAllPages<TestItem, TestParams>(mockFetch, {}, 3, mockProgress);

// Verify progress was called for each page
expect(mockProgress).toHaveBeenCalledTimes(4); // Initial + 3 pages
expect(mockProgress).toHaveBeenNthCalledWith(1, 1, 3, 9); // First page
expect(mockProgress).toHaveBeenLastCalledWith(3, 3, 9); // Final page
});

test('should throw an error when the fetch function fails', async () => {
// Mock fetch function that throws an error
const mockFetch = vi.fn().mockRejectedValue(new Error('API Error'));

// Attempt to call the function and expect it to throw
await expect(fetchAllPages<TestItem, TestParams>(mockFetch, {}, 10)).rejects.toThrow(
'API Error',
);

// Verify the fetch was called
expect(mockFetch).toHaveBeenCalledTimes(1);
});

test('should throw an error when the first page fetch returns undefined', async () => {
// Mock fetch function that returns undefined
const mockFetch = vi.fn().mockResolvedValue(undefined);

// Attempt to call the function and expect it to throw
await expect(fetchAllPages<TestItem, TestParams>(mockFetch, {}, 10)).rejects.toThrow(
'Failed to fetch data',
);

// Verify the fetch was called
expect(mockFetch).toHaveBeenCalledTimes(1);
});

test('should handle missing page data in subsequent pages', async () => {
// Mock data for pages
const page1Data: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

// Mock fetch function that returns undefined for page 2
const mockFetch = vi
.fn()
.mockImplementationOnce(() =>
Promise.resolve({
data: page1Data,
totalPages: 2,
totalItems: 4,
} as PaginatedResponse<TestItem>),
)
.mockImplementationOnce(() => Promise.resolve(undefined));

// Call the function
const result = await fetchAllPages<TestItem, TestParams>(mockFetch, {}, 2);

// Verify the fetch was called twice
expect(mockFetch).toHaveBeenCalledTimes(2);

// Verify we only got page 1 data since page 2 returned undefined
expect(result).toEqual(page1Data);
});

test('should handle missing data property in subsequent pages', async () => {
// Mock data for pages
const page1Data: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

// Mock fetch function that returns a response without data for page 2
const mockFetch = vi
.fn()
.mockImplementationOnce(() =>
Promise.resolve({
data: page1Data,
totalPages: 2,
totalItems: 4,
} as PaginatedResponse<TestItem>),
)
.mockImplementationOnce(() =>
Promise.resolve({
totalPages: 2,
totalItems: 4,
// Missing data property
} as any),
);

// Call the function
const result = await fetchAllPages<TestItem, TestParams>(mockFetch, {}, 2);

// Verify the fetch was called twice
expect(mockFetch).toHaveBeenCalledTimes(2);

// Verify we only got page 1 data since page 2 had no data property
expect(result).toEqual(page1Data);
});

test('should use default page size when not provided', async () => {
// Mock data
const mockData: TestItem[] = [{ id: 1, name: 'Item 1' }];

// Mock fetch function
const mockFetch = vi.fn().mockResolvedValue({
data: mockData,
totalPages: 1,
totalItems: 1,
} as PaginatedResponse<TestItem>);

// Call the function without specifying pageSize
await fetchAllPages<TestItem, TestParams>(mockFetch, {});

// Verify the fetch was called with the default page size
expect(mockFetch).toHaveBeenCalledWith({
page: { number: 1, size: 50 }, // Default is 50
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fetch-all-pages';
14 changes: 10 additions & 4 deletions apps/backoffice-v2/src/common/utils/fetcher/fetcher.ts
Original file line number Diff line number Diff line change
@@ -22,9 +22,14 @@ export const fetcher: IFetcher = async ({
}) => {
const controller = new AbortController();
const { signal } = controller;
const timeoutRef = setTimeout(() => {
controller.abort(`Request timed out after ${timeout}ms`);
}, timeout);

const isDevelopment = import.meta.env.DEV;
const timeoutRef = !isDevelopment
? setTimeout(() => {
controller.abort(`Request timed out after ${timeout}ms`);
}, timeout)
: null;

const [res, fetchError] = await handlePromise(
fetch(url, {
...options,
@@ -34,7 +39,8 @@ export const fetcher: IFetcher = async ({
headers: isFormData ? undefined : headers,
}),
);
clearTimeout(timeoutRef);

if (timeoutRef) clearTimeout(timeoutRef);

if (fetchError) {
console.error(fetchError);
2 changes: 2 additions & 0 deletions apps/backoffice-v2/src/common/utils/is-csv/is-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isCsv = <T extends { fileType: string }>(document: T) =>
document?.fileType === 'text/csv' || document?.fileType === 'application/csv';
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FunctionComponent } from 'react';
import type { FunctionComponent } from 'react';
import { Navigate, Outlet } from 'react-router-dom';

import { Header } from '@/common/components/organisms/Header';
import { FullScreenLoader } from '@/common/components/molecules/FullScreenLoader/FullScreenLoader';
import { SidebarInset, SidebarProvider } from '@/common/components/organisms/Sidebar/Sidebar';
import { AppSidebar } from './components/AppSidebar';
import { useAuthenticatedLayoutLogic } from './hooks/useAuthenticatedLayoutLogic/useAuthenticatedLayoutLogic';

export const AuthenticatedLayout: FunctionComponent = () => {
@@ -26,36 +27,17 @@ export const AuthenticatedLayout: FunctionComponent = () => {
}

return (
<div className="drawer drawer-mobile">
<input id="app-drawer" type="checkbox" className="drawer-toggle" />
<div className={`drawer-content`}>
<main className={`h-full`}>
<Outlet />
</main>
</div>
<div className={`drawer-side w-[250px]`}>
<label htmlFor="app-drawer" className="drawer-overlay"></label>
<Header />
</div>
<label
htmlFor="app-drawer"
className="btn btn-square drawer-button fixed z-50 bottom-right-6 lg:hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</label>
</div>
<SidebarProvider
style={{
'--sidebar-width-mobile': '12rem',
'--sidebar-width': '18rem',
'--sidebar-width-xl': '21rem',
}}
>
<AppSidebar />
<SidebarInset>
<Outlet />
</SidebarInset>
</SidebarProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { env } from '../../../../common/env/env';
import { LoaderFunction, redirect } from 'react-router-dom';
import { env } from '@/common/env/env';
import { queryClient } from '@/lib/react-query/query-client';
import { magicLinkSignIn } from '@/domains/auth/fetchers';
import { authQueryKeys } from '../../query-keys';
import { queryClient } from '../../../../lib/react-query/query-client';
import { filtersQueryKeys } from '../../../filters/query-keys';
import { LoaderFunction } from 'react-router-dom';

export const authenticatedLayoutLoader: LoaderFunction = async () => {
export const authenticatedLayoutLoader: LoaderFunction = async ({ request }) => {
if (!env.VITE_AUTH_ENABLED) return null;

const url = new URL(request.url);
const token = url.searchParams.get('token');

if (token) {
try {
await queryClient.fetchQuery(['magic-link-auth', token], ({ queryKey }) =>
magicLinkSignIn({ token: queryKey[1]! }),
);
} catch (e) {
console.error('Error using magic link', e);
return redirect(`/en/auth/sign-in`);
}
}

const authenticatedUser = authQueryKeys.authenticatedUser();
const session = await queryClient.ensureQueryData(
authenticatedUser.queryKey,
@@ -18,5 +33,11 @@ export const authenticatedLayoutLoader: LoaderFunction = async () => {
const filtersList = filtersQueryKeys.list();
await queryClient.ensureQueryData(filtersList.queryKey, filtersList.queryFn);

if (token) {
const newUrl = new URL(request.url);
newUrl.searchParams.delete('token');
return redirect(newUrl.toString());
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ComponentProps } from 'react';

import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarTrigger,
} from '@/common/components/organisms/Sidebar/Sidebar';
import { NavFooter } from './NavFooter';
import { NavLogo } from './NavLogo';
import { NavMain } from './NavMain';
import { NavIntroduction } from './NavIntroduction';

export const AppSidebar = ({ ...props }: ComponentProps<typeof Sidebar>) => {
return (
<Sidebar
collapsible="icon"
className="bg-[#F4F6FD] px-2 group-data-[collapsible=icon]:px-0"
{...props}
>
<SidebarTrigger className="absolute right-2 top-2 z-10 d-6 group-data-[collapsible=icon]:right-3" />

<SidebarHeader>
<NavLogo className="h-24 group-data-[collapsible=icon]:hidden" />
</SidebarHeader>

<SidebarContent>
<NavMain className="group-data-[collapsible=icon]:mt-24 2xl:mt-12 group-data-[collapsible=icon]:2xl:mt-36" />
</SidebarContent>

<SidebarFooter>
<NavIntroduction />

<NavFooter />
</SidebarFooter>
</Sidebar>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Settings2Icon } from 'lucide-react';
import { useCallback, useMemo } from 'react';

import { LogOutSvg } from '@/common/components/atoms/icons';
import { UserAvatar } from '@/common/components/atoms/UserAvatar/UserAvatar';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/common/components/organisms/Sidebar/Sidebar';
import { useAuthContext } from '@/domains/auth/context/AuthProvider/hooks/useAuthContext/useAuthContext';
import { useSignOutMutation } from '@/domains/auth/hooks/mutations/useSignOutMutation/useSignOutMutation';
import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { NavItem } from './NavItem';

export const NavFooter = () => {
const { mutate: signOut } = useSignOutMutation();
const { signOutOptions } = useAuthContext();
const onSignOut = useCallback(
() =>
signOut({
redirect: signOutOptions?.redirect,
callbackUrl: signOutOptions?.callbackUrl,
}),
[signOutOptions?.redirect, signOutOptions?.callbackUrl, signOut],
);
const { data: customer, isLoading: isCustomerLoading } = useCustomerQuery();
const { data: session } = useAuthenticatedUserQuery();
const fullName = useMemo(
() => `${session?.user?.firstName} ${session?.user?.lastName}`,
[session?.user?.firstName, session?.user?.lastName],
);

return (
<SidebarMenu>
{customer?.config?.isDemoAccount && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<NavItem
navItem={{
text: 'Configurations',
icon: Settings2Icon,
premium: {
caption: 'Configure your risk tools to perform according to your risk appetite',
checkList: [
'Assign risk weights',
'Align with your policies',
'Request new categories',
],
videoLink:
'https://www.loom.com/embed/aa86729a657140c1988f0769ce8a29f8?sid=b22db479-b6c8-46e9-93be-c4e00751d8a8',
},
key: 'nav-item-documents-verifications',
}}
className="mb-6 cursor-default group-data-[collapsible=icon]:!px-0"
/>
</SidebarMenuButton>
</SidebarMenuItem>
)}

<SidebarMenuItem className="mb-2 mt-auto flex flex-col space-y-2 px-2 group-data-[collapsible=icon]:px-0">
<SidebarMenuButton className="-ml-0.5 flex h-9 items-center gap-x-2 rounded-md text-sm font-medium normal-case">
<UserAvatar fullName={fullName} avatarUrl={session?.user?.avatarUrl} />
<div className="text-sm">{fullName}</div>
</SidebarMenuButton>

<SidebarMenuButton
className="flex h-9 items-center gap-x-2 rounded-md py-0 text-sm font-medium normal-case"
onClick={onSignOut}
>
<LogOutSvg className="h-4 w-4" />
Log out
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { PlayCircleIcon } from 'lucide-react';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { Skeleton } from '@ballerine/ui';

export const NavIntroduction = () => {
const { data: customer } = useCustomerQuery();

// Only show the introduction in demo accounts
if (!customer?.config?.isDemoAccount) {
return null;
}

return (
<div className="mb-6 px-2 transition-opacity group-data-[collapsible=icon]:opacity-0">
<div className="rounded-lg border bg-white p-3 shadow-sm">
<div className="flex items-center gap-2">
<div className="w-2/3">
<h3 className="text-sm font-bold">Introduction</h3>
<p className="text-xs text-gray-700">
Watch this quick introduction to learn how to use Ballerine effectively.
</p>
</div>
<div className="relative w-3/4 overflow-hidden rounded-md">
{/* Video player with thumbnail and iframe */}
<div className="relative">
{/* Thumbnail with play button */}
<button
className="group relative w-full"
onClick={() => {
const thumbnail = document.getElementById('video-thumbnail');
const iframe = document.getElementById('video-iframe');

if (thumbnail && iframe) {
thumbnail.style.display = 'none';
iframe.style.display = 'block';

// Update iframe src to autoplay
const iframeElement = iframe.querySelector('iframe');
if (iframeElement) {
const currentSrc = iframeElement.src;
iframeElement.src = currentSrc + '&autoplay=true';
}
}
}}
id="video-thumbnail"
>
<img
src="https://cdn.loom.com/sessions/thumbnails/7cd69b5e2db24e81ace760cc38b3d7dc-8dd5afc805842339-full-play.gif"
alt="Introduction video thumbnail"
className="w-full rounded-md"
/>
<div className="absolute inset-0 flex items-center justify-center">
<PlayCircleIcon className="h-10 w-10 text-white opacity-80 transition-opacity group-hover:opacity-100" />
</div>
</button>

{/* Video iframe (hidden initially) */}
<div
id="video-iframe"
style={{
position: 'relative',
paddingBottom: '56.25%',
height: 0,
display: 'none',
}}
>
<Skeleton className="absolute inset-0 size-full" />
<iframe
src="https://www.loom.com/embed/7cd69b5e2db24e81ace760cc38b3d7dc?sid=69a0ffbf-bd57-4e88-b9db-cbf819da21d3&hideEmbedTopBar=true"
frameBorder="0"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import {
CollapsibleTrigger,
HoverCard,
HoverCardContent,
HoverCardTrigger,
Skeleton,
} from '@ballerine/ui';
import { ChevronRightIcon, CircleCheckIcon, CrownIcon } from 'lucide-react';
import { forwardRef, type ReactNode } from 'react';
import { NavLink } from 'react-router-dom';

import { SidebarMenuButton } from '@/common/components/organisms/Sidebar/Sidebar';
import { ctw } from '@/common/utils/ctw/ctw';
import { TRouteWithOptionalIcon, TRouteWithoutChildren } from '../types';

const PremiumNavItemHoverCard = ({
premiumProps,
navItemTitle,
navItemText,
children,
}: {
premiumProps: NonNullable<TRouteWithoutChildren['premium']>;
navItemTitle?: string;
navItemText?: string;
children: ReactNode;
}) => {
const { caption, checkList, videoLink } = premiumProps;

return (
<HoverCard openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className="cursor-default space-y-4 font-normal normal-case"
onPointerDown={e => e.stopPropagation()}
>
<div className="relative">
{videoLink ? (
<div style={{ position: 'relative', paddingBottom: '56.25%', height: 0 }}>
<Skeleton className="absolute inset-0 size-full" />
<iframe
src={videoLink}
frameBorder="0"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
/>
</div>
) : (
<div className="mb-2 flex items-center justify-between">
<h3 className="text-sm font-medium text-slate-800 2xl:text-base">
{navItemText || 'Premium Feature'}
</h3>
</div>
)}

<CrownIcon className="absolute right-1 top-1 -translate-y-1/3 translate-x-1/3 rounded-full bg-[#584EC5] stroke-primary-foreground p-1.5 d-8" />
</div>

{navItemTitle && (
<p
className={ctw(
'2xl:text-md text-sm font-medium text-slate-800',
'hidden group-data-[collapsible=icon]:block',
)}
>
{navItemTitle}
</p>
)}

<p className="text-xs text-slate-600 2xl:text-sm">{caption}</p>
<div className="space-y-2">
{checkList.map(checkListItem => (
<div
key={checkListItem}
className="flex items-center gap-x-1 text-xs text-slate-800 2xl:text-sm"
>
<CircleCheckIcon className="stroke-slate-500 d-4" />
{checkListItem}
</div>
))}
</div>
</HoverCardContent>
</HoverCard>
);
};

const baseNavItemWrapperClassName = 'flex items-center gap-x-2 w-full cursor-default h-full';
const NavItemWrapper = ({
navItem,
children,
className,
}: {
navItem: TRouteWithOptionalIcon;
children: ReactNode;
className?: string;
}) => {
let NavItemElement = (
<div
className={ctw(
baseNavItemWrapperClassName,
{ 'cursor-pointer': 'children' in navItem },
className,
)}
>
{children}
</div>
);

if ('href' in navItem && navItem.href) {
NavItemElement = (
<NavLink
to={navItem.href}
className={ctw(baseNavItemWrapperClassName, 'cursor-pointer', className)}
>
{children}
</NavLink>
);
}

if (navItem.premium?.href) {
NavItemElement = (
<a
href={navItem.premium.href}
className={ctw(baseNavItemWrapperClassName, 'cursor-pointer', className)}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}

if (navItem.premium) {
return (
<PremiumNavItemHoverCard
premiumProps={navItem.premium}
navItemTitle={navItem.text}
navItemText={navItem.text}
>
{NavItemElement}
</PremiumNavItemHoverCard>
);
}

return NavItemElement;
};

const NavItem = forwardRef<
React.ElementRef<typeof CollapsibleTrigger>,
React.ComponentPropsWithoutRef<typeof CollapsibleTrigger> & {
navItem: TRouteWithOptionalIcon;
linkClassName?: string;
}
>(({ navItem, className, linkClassName, ...props }, ref) => {
const { text, premium } = navItem;

return (
<SidebarMenuButton
ref={ref}
className={ctw(
'flex h-auto w-full items-center gap-x-2 rounded-md text-sm font-bold capitalize text-slate-400 2xl:text-base',
'group-data-[collapsible=icon]:h-9 group-data-[collapsible=icon]:!p-0',
'duration-50 transition-colors',
{
'text-slate-400/60': premium,
'hover:bg-slate-200 hover:text-primary active:bg-primary-foreground active:text-primary':
!premium,
'group-data-[collapsible=icon]:hidden': 'children' in navItem,
},
className,
)}
tooltip={!premium ? text : undefined}
{...props}
>
<NavItemWrapper
navItem={navItem}
className={ctw('group-data-[collapsible=icon]:!px-2', linkClassName)}
>
{'icon' in navItem && navItem.icon && (
<navItem.icon className="shrink-0 d-5 group-data-[collapsible=icon]:-ml-0.5" />
)}
{text}
{'children' in navItem && (
<ChevronRightIcon className="ml-auto transition-transform duration-200 d-4 group-data-[state=open]/collapsible:rotate-90" />
)}

{premium && <CrownIcon className="ml-auto stroke-[#968FDE] d-4 2xl:d-5" />}
</NavItemWrapper>
</SidebarMenuButton>
);
});
NavItem.displayName = 'NavItem';

export { NavItem };
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Skeleton } from '@ballerine/ui';
import { FunctionComponent } from 'react';
import { Link } from 'react-router-dom';

import { AspectRatio } from '@/common/components/atoms/AspectRatio/AspectRatio';
import { BallerineLogo } from '@/common/components/atoms/icons';
import { env } from '@/common/env/env';
import { useRedirectToRootUrl } from '@/common/hooks/useRedirectToRootUrl/useRedirectToRootUrl';
import { ctw } from '@/common/utils/ctw/ctw';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';

const LogoContent = () => {
const { data: customer, isLoading } = useCustomerQuery();
const imageUrl = customer?.logoImageUri ?? env.VITE_IMAGE_LOGO_URL;

if (isLoading) {
return <Skeleton className="h-24 w-full" />;
}

if (imageUrl) {
return (
<AspectRatio ratio={2 / 1}>
<img src={imageUrl} className="d-full object-contain object-center" />
</AspectRatio>
);
}

return <BallerineLogo />;
};

export const NavLogo: FunctionComponent<{ className?: string }> = ({ className }) => {
const urlToRoot = useRedirectToRootUrl();

return (
<Link to={urlToRoot} className={ctw('w-full cursor-pointer pr-12', className)}>
<LogoContent />
</Link>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@ballerine/ui';
import type { FunctionComponent } from 'react';

import {
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubItem,
} from '@/common/components/organisms/Sidebar/Sidebar';
import { ctw } from '@/common/utils/ctw/ctw';
import { useSidebarItems } from '../hooks/useSidebarItems/useSidebarItems';
import { NavItem } from './NavItem';

export const NavMain: FunctionComponent<{ className?: string }> = ({ className }) => {
const { navItems, pathname, filterId, checkIsActiveFilterGroup } = useSidebarItems();

return (
<SidebarGroup>
<SidebarMenu className={ctw('gap-2', className)}>
{navItems.map(navItem => {
if ('children' in navItem) {
const isActiveFilterGroup = checkIsActiveFilterGroup(navItem);

return (
<Collapsible
asChild
defaultOpen={isActiveFilterGroup}
key={navItem.key}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<NavItem
navItem={navItem}
className={ctw('p-0', {
'bg-background text-primary': isActiveFilterGroup,
})}
linkClassName="p-2 group-data-[collapsible=icon]:p-0"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub className="pt-2">
{navItem.children.map(subItem => (
<SidebarMenuSubItem key={subItem.key}>
<NavItem
navItem={subItem}
className={ctw('p-0', {
'font-semibold text-[#20232E]': subItem.filterId === filterId,
'text-[#8990AC] aria-[current=page]:font-normal':
subItem.filterId && subItem.filterId !== filterId,
})}
linkClassName="p-2"
/>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
}

return (
<SidebarMenuItem key={navItem.key}>
<NavItem
navItem={navItem}
className={ctw('p-0', {
'bg-background text-primary': navItem.href && pathname.includes(navItem.href),
})}
linkClassName="p-2 group-data-[collapsible=icon]:p-0"
/>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {
BuildingIcon,
FileCheck2Icon,
GavelIcon,
GoalIcon,
HomeIcon,
LayersIcon,
MonitorDotIcon,
UserRoundSearchIcon,
UsersIcon,
} from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';

import { useFilterId } from '@/common/hooks/useFilterId/useFilterId';
import { useLocale } from '@/common/hooks/useLocale/useLocale';
import { TRoute, TRouteWithChildren } from '@/domains/auth/components/AuthenticatedLayout/types';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { useFiltersQuery } from '@/domains/filters/hooks/queries/useFiltersQuery/useFiltersQuery';

export const useSidebarItems = () => {
const { data: filters } = useFiltersQuery();
const locale = useLocale();
const filterId = useFilterId();
const individualsFilters = useMemo(
() => filters?.filter(({ entity }) => entity === 'individuals'),
[filters],
);
const businessesFilters = useMemo(
() => filters?.filter(({ entity }) => entity === 'businesses'),
[filters],
);
const { data: customer } = useCustomerQuery();

const { pathname } = useLocation();
const checkIsActiveFilterGroup = useCallback(
(navItem: TRouteWithChildren) => {
return navItem.children?.some(
childNavItem => childNavItem.filterId === filterId || childNavItem.href === pathname,
);
},
[filterId, pathname],
);

const navItems: TRoute[] = customer?.config?.isDemoAccount
? [
{
text: 'Home',
icon: HomeIcon,
href: `/${locale}/home`,
key: 'nav-item-home',
},
{
text: 'Web Presence',
icon: MonitorDotIcon,
href: `/${locale}/merchant-monitoring`,
key: 'nav-item-web-presence',
},
{
text: 'Full Onboarding (Example)',
icon: LayersIcon,
href: `/${locale}/case-management/entities`,
key: 'nav-item-full-onboarding',
},
{
text: 'KYB & UBOs',
icon: BuildingIcon,
premium: {
caption: 'Verify businesses, activity, and ownership to stay compliant.',
checkList: [
'Retrieve company registry data',
'Validate existence and status',
'Identify key stakeholders',
],
},
key: 'nav-item-kyb-ubos',
},
{
text: 'Identity Verification',
icon: UserRoundSearchIcon,
premium: {
caption: 'Authenticate individuals quickly, using highest standards.',
checkList: [
'Validate government-issued IDs',
'Biometric and liveness checks',
'Global coverage',
],
},
key: 'nav-item-identity-verification',
},
{
text: 'Sanctions Screening',
icon: GavelIcon,
premium: {
caption: 'Screen entities against global watchlists.',
checkList: [
'Real-time sanctions checks',
'Sanctions, PEPs, & adverse media',
'Customizable preferences',
],
},
key: 'nav-item-sanctions-screening',
},
{
text: 'Documents Verification',
icon: FileCheck2Icon,
premium: {
caption: 'Extract data, classify, validate and verify documents.',
checkList: [
'All types of documents',
'Works in every language',
'Detect faults and fakes',
],
},
key: 'nav-item-documents-verifications',
},
]
: [
{
text: 'Home',
icon: HomeIcon,
href: `/${locale}/home`,
key: 'nav-item-Home',
},
...(customer?.config?.isMerchantMonitoringEnabled
? [
{
text: 'Web Presence',
icon: MonitorDotIcon,
href: `/${locale}/merchant-monitoring`,
key: 'nav-item-merchant-monitoring',
},
]
: []),
{
text: 'Businesses',
icon: BuildingIcon,
children:
businessesFilters?.map(({ id, name }) => ({
filterId: id,
text: name,
key: `nav-item-${id}`,
href: `/${locale}/case-management/entities?filterId=${id}`,
})) ?? [],
key: 'nav-item-businesses',
},
{
text: 'Individuals',
icon: UsersIcon,
children: [
...(individualsFilters?.map(({ id, name }) => ({
filterId: id,
text: name,
href: `/${locale}/case-management/entities?filterId=${id}`,
key: `nav-item-${id}`,
})) ?? []),
],
key: 'nav-item-individuals',
},
{
text: 'Transaction Monitoring',
icon: GoalIcon,
children: [
{
text: 'Alerts',
href: `/${locale}/transaction-monitoring/alerts`,
key: 'nav-item-alerts',
},
],
key: 'nav-item-transaction-monitoring',
},
];

return {
navItems,
filterId,
pathname,
checkIsActiveFilterGroup,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { LucideIcon } from 'lucide-react';

export type TRouteBase = {
text: string;
key: string;
premium?: {
caption: string;
checkList: string[];
videoLink?: string;
href?: string;
};
};

export type TRouteWithoutChildrenBase = TRouteBase & {
filterId?: string;
href?: string;
};

export type TRouteWithoutChildren = TRouteWithoutChildrenBase & {
icon: LucideIcon;
};

export type TRouteWithChildren = TRouteBase & {
children: TRouteWithoutChildrenBase[];
};

export type TRoute = TRouteWithChildren | TRouteWithoutChildren;
export type TRouteWithOptionalIcon = Omit<TRoute, 'icon'> & {
icon?: TRouteWithoutChildren['icon'];
};
26 changes: 21 additions & 5 deletions apps/backoffice-v2/src/domains/auth/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ISignInProps } from './hooks/mutations/useSignInMutation/interfaces';
import { apiClient } from '../../common/api-client/api-client';
import { apiClient } from '@/common/api-client/api-client';
import { z } from 'zod';
import { handleZodError } from '../../common/utils/handle-zod-error/handle-zod-error';
import { Method } from '../../common/enums';
import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error';
import { Method } from '@/common/enums';
import { AuthenticatedUserSchema } from './validation-schemas';
import posthog from 'posthog-js';

export const fetchSignOut = async ({ callbackUrl }: ISignInProps) => {
export const signOut = async ({ callbackUrl }: ISignInProps) => {
const [session, error] = await apiClient({
endpoint: `auth/logout`,
method: Method.POST,
@@ -26,7 +26,7 @@ export const fetchSignOut = async ({ callbackUrl }: ISignInProps) => {
return handleZodError(error, session);
};

export const fetchSignIn = async ({ callbackUrl, body }: ISignInProps) => {
export const signIn = async ({ callbackUrl, body }: ISignInProps) => {
const [session, error] = await apiClient({
endpoint: 'auth/login',
method: Method.POST,
@@ -51,6 +51,22 @@ export const fetchSignIn = async ({ callbackUrl, body }: ISignInProps) => {
return handleZodError(error, session);
};

export const magicLinkSignIn = async ({ token }: { token: string }) => {
const [session, error] = await apiClient({
endpoint: 'auth/magic-link-login',
method: Method.POST,
schema: z.any(),
body: {
token,
},
options: {
headers: {},
},
});

return handleZodError(error, session);
};

export const fetchAuthenticatedUser = async () => {
const [session, error] = await apiClient({
endpoint: `auth/session`,
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useLocation, useNavigate } from 'react-router-dom';
import { ISignInProps } from './interfaces';
import { fetchSignIn } from '../../../fetchers';
import { signIn } from '../../../fetchers';
import { authQueryKeys } from '../../../query-keys';

export const useSignInMutation = () => {
@@ -12,7 +12,7 @@ export const useSignInMutation = () => {

return useMutation({
mutationFn: ({ callbackUrl, body }: ISignInProps) =>
fetchSignIn({
signIn({
callbackUrl,
body,
}),
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useLocation, useNavigate } from 'react-router-dom';
import { ISignInProps } from '../useSignInMutation/interfaces';
import { authQueryKeys } from '../../../query-keys';
import { fetchSignOut } from '../../../fetchers';
import { signOut } from '../../../fetchers';
import { customerQueryKeys } from '@/domains/customer/query-keys';

export const useSignOutMutation = () => {
@@ -14,7 +14,7 @@ export const useSignOutMutation = () => {

return useMutation({
mutationFn: ({ callbackUrl }: ISignInProps) =>
fetchSignOut({
signOut({
callbackUrl,
}),
onMutate: () => {
1 change: 1 addition & 0 deletions apps/backoffice-v2/src/domains/auth/validation-schemas.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ export const AuthenticatedUserSchema = z
firstName: z.string(),
lastName: z.string(),
avatarUrl: z.string().nullable().optional(),
lastActiveAt: z.string().datetime().nullable().optional(),
})
.transform(({ firstName, lastName, ...other }) => ({
...other,
Loading