@vtex/faststore-plugin-buyer-portal 2.0.19 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/docs/i18n/locales.json +39 -0
  3. package/docs/i18n/migration-inventory.md +78 -0
  4. package/package.json +1 -1
  5. package/specs/analytics-actor-type.md +243 -0
  6. package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +28 -15
  7. package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +15 -12
  8. package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +6 -4
  9. package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +5 -1
  10. package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +5 -2
  11. package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +4 -1
  12. package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +5 -2
  13. package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +4 -1
  14. package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +12 -15
  15. package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +11 -8
  16. package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +27 -20
  17. package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +28 -26
  18. package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +9 -6
  19. package/src/features/shared/hooks/analytics/useAnalytics.ts +1 -1
  20. package/src/features/shared/services/logger/analytics/__tests__/deriveActorType.test.ts +30 -0
  21. package/src/features/shared/services/logger/analytics/__tests__/mergeActorTypeIntoEvent.test.ts +83 -0
  22. package/src/features/shared/services/logger/analytics/analytics.ts +2 -24
  23. package/src/features/shared/services/logger/analytics/deriveActorType.ts +24 -0
  24. package/src/features/shared/services/logger/analytics/mergeActorTypeIntoEvent.ts +29 -0
  25. package/src/features/shared/services/logger/analytics/types.ts +5 -0
  26. package/src/features/shared/services/logger/analytics/useDataIngestionApi.ts +32 -0
  27. package/src/features/shared/utils/constants.ts +1 -1
  28. package/src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx +9 -10
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.21] - 2026-08-13
11
+
12
+ ### Changed
13
+ - Replaced hardcoded, user-facing strings in the `contracts` feature (multi-contract/SUMA drawers, list, selection bar, listing/information layouts) and in `RolesMultiSelect` with `useLocalization()`/`t()` calls, adding the corresponding keys to the CMS message catalog
14
+
15
+ ## [2.0.20] - 2026-08-12
16
+
17
+ ### Added
18
+
19
+ - Analytics events now include a derived `metadata.actor_type` (`internal` | `external`) based on the logged-in user's email domain (`@vtex.com` → internal), with no email or identity fields in the payload ([B2BTEAM-3639](https://vtex-dev.atlassian.net/browse/B2BTEAM-3639))
20
+
10
21
  ## [2.0.19] - 2026-08-04
11
22
 
12
23
  ### Changed
@@ -821,7 +832,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
821
832
  - Add CHANGELOG file
822
833
  - Add README file
823
834
 
824
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.19...HEAD
835
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.21...HEAD
825
836
  [1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
826
837
  [1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
827
838
  [1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
@@ -919,6 +930,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
919
930
  [2.0.10]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.9...v2.0.10
920
931
  [2.0.9]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.9
921
932
 
933
+ [2.0.21]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.20...v2.0.21
934
+ [2.0.20]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.19...v2.0.20
922
935
  [2.0.19]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.18...v2.0.19
923
936
  [2.0.18]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.17...v2.0.18
924
937
  [2.0.17]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.16...v2.0.17
@@ -14,6 +14,7 @@
14
14
  "shared.buttons.create": "Create",
15
15
  "shared.buttons.tryAgain": "Try again",
16
16
  "shared.buttons.loadMore": "Load More",
17
+ "shared.buttons.gotIt": "Got it",
17
18
  "shared.labels.loading": "Loading...",
18
19
  "shared.labels.name": "Name",
19
20
  "shared.labels.email": "Email",
@@ -377,6 +378,8 @@
377
378
  "users.dropdown.resetPassword": "Reset password",
378
379
  "users.dropdown.changeOrganizationalUnit": "Change organizational unit",
379
380
  "users.components.UsersCard.manageUsers": "Manage users",
381
+ "users.rolesMultiSelect.customTag": "Custom",
382
+ "users.rolesMultiSelect.noMatchingRoles": "No matching roles",
380
383
  "budgets.titles.budgets": "Budgets",
381
384
  "budgets.titles.financeAndCompliance": "Finance and Compliance",
382
385
  "budgets.labels.remainingBalance": "Remaining balance",
@@ -692,6 +695,42 @@
692
695
  "contracts.actions.viewContracts": "View contracts",
693
696
  "contracts.dropdown.open": "Open",
694
697
  "contracts.dropdown.edit": "Edit",
698
+ "contracts.labels.contract": "contract",
699
+ "contracts.labels.contracts": "contracts",
700
+ "contracts.labels.selectAllCount": "Select all ({0})",
701
+ "contracts.labels.selectedCount": "{0} selected",
702
+ "contracts.actions.setAsDefault": "Set as default",
703
+ "contracts.addDrawer.title": "Add contracts",
704
+ "contracts.addDrawer.description": "Add available contracts to {0}",
705
+ "contracts.addDrawer.paginationRange": "{0}–{1} of {2}",
706
+ "contracts.addDrawer.loading": "Loading contracts…",
707
+ "contracts.addDrawer.noSearchResults": "No contracts match your search.",
708
+ "contracts.addDrawer.noneAvailable": "No contracts available to add.",
709
+ "contracts.removeDrawer.title": "Remove {0}",
710
+ "contracts.removeDrawer.question": "Are you sure you want to remove {0} {1} from {2}?",
711
+ "contracts.removeDrawer.warning": "Removed contracts can be added again later, but any specific configurations for these contracts within this organizational unit will be lost. This will prevent users in this organization unit from accessing these contracts during store navigation and checkout.",
712
+ "contracts.removeDrawer.unableTitle": "Unable to remove contracts",
713
+ "contracts.removeDrawer.error": "An error occurred while removing the contract. Please try again.",
714
+ "contracts.removeDrawer.minimumContractWarning": "Every organizational unit must have at least one contract available. This ensures that users within {0} can always access store features like navigation and checkout.",
715
+ "contracts.removeDrawer.deselectHint": "To proceed, deselect at least one contract to keep it in this organizational unit.",
716
+ "contracts.removeDrawer.unableDeselectHint": "To proceed, deselect at least one contract to keep it in this {0}. When an organizational unit has only one contract, it is automatically set as the default.",
717
+ "contracts.listing.emptyState": "No contracts found for this organizational unit.",
718
+ "contracts.listing.interval": "{0} — {1} of {2}",
719
+ "contracts.listing.infoTooltip": "Contracts define the commercial rules applied during purchasing, such as addresses, payment methods, credit cards and other checkout settings.",
720
+ "contracts.nav.basicInformation": "Basic information",
721
+ "contracts.nav.assortment": "Assortment",
722
+ "contracts.nav.costCenters": "Cost centers",
723
+ "contracts.nav.departments": "Departments",
724
+ "contracts.nav.poNumbers": "PO numbers",
725
+ "contracts.details.createdDate": "Created date",
726
+ "contracts.toasts.addError": "Failed to add contracts. Please try again.",
727
+ "contracts.toasts.switchError": "Failed to switch contract. Please try again.",
728
+ "contracts.toasts.setDefaultSuccess": "Default contract updated successfully",
729
+ "contracts.toasts.setDefaultError": "Failed to set default contract. Please try again.",
730
+ "contracts.toasts.removeSuccess": "{0} contract{1} removed successfully",
731
+ "contracts.toasts.removeError": "Failed to remove contracts. Please try again.",
732
+ "contracts.toasts.refreshError": "Contracts removed, but we couldn't refresh your active contract. Please reload the page.",
733
+ "contracts.toasts.loadError": "Failed to load contract information.",
695
734
  "profile.titles.profile": "Profile",
696
735
  "profile.titles.details": "Details",
697
736
  "profile.labels.creationDate": "Creation Date",
@@ -487,6 +487,9 @@ Strings defined in `src/features/shared/` — canonical source for all `shared.*
487
487
  | `shared.buttons.delete` | "Delete" | `src/features/users/components/UserDropdownMenu/UserDropdownMenu.tsx` | 100 |
488
488
  | `users.components.UsersCard.manageUsers` | "Manage users" | `src/features/users/components/UsersCard/UsersCard.tsx` | 32 |
489
489
  | `users.titles.users` | "Users" | `src/features/users/components/UsersCard/UsersCard.tsx` | 35 |
490
+ | `users.forms.roles` | "Roles" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 244 |
491
+ | `users.rolesMultiSelect.customTag` | "Custom" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 302 |
492
+ | `users.rolesMultiSelect.noMatchingRoles` | "No matching roles" | `src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx` | 258 |
490
493
 
491
494
  ---
492
495
 
@@ -914,6 +917,81 @@ Strings defined in `src/features/shared/` — canonical source for all `shared.*
914
917
  | `contracts.actions.viewContracts` | "View contracts" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 36 |
915
918
  | `contracts.dropdown.open` | "Open" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 81 |
916
919
  | `contracts.dropdown.edit` | "Edit" | `src/features/contracts/components/ContractsCard/ContractsCard.tsx` | 90 |
920
+ | `contracts.toasts.addError` | "Failed to add contracts. Please try again." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 53 |
921
+ | `contracts.addDrawer.title` | "Add contracts" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 145 |
922
+ | `contracts.addDrawer.description` | "Add available contracts to {0}" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 151 |
923
+ | `shared.placeholders.search` | "Search" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 161 |
924
+ | `contracts.addDrawer.paginationRange` | "{0}–{1} of {2}" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 190 |
925
+ | `contracts.addDrawer.loading` | "Loading contracts…" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 218 |
926
+ | `contracts.addDrawer.noSearchResults` | "No contracts match your search." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 224 |
927
+ | `contracts.addDrawer.noneAvailable` | "No contracts available to add." | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 225 |
928
+ | `contracts.labels.selectAllCount` | "Select all ({0})" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 251 |
929
+ | `contracts.labels.selectedCount` | "{0} selected" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 299 |
930
+ | `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 309 |
931
+ | `shared.buttons.add` | "Add" | `src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx` | 318 |
932
+ | `contracts.labels.contract` | "contract" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 17 |
933
+ | `contracts.labels.contracts` | "contracts" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 17 |
934
+ | `contracts.removeDrawer.title` | "Remove {0}" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 22 |
935
+ | `contracts.removeDrawer.question` | "Are you sure you want to remove {0} {1} from {2}?" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 27 |
936
+ | `contracts.removeDrawer.warning` | "Removed contracts can be added again later, but any specific configurations for these contracts within this organizational unit will be lost..." | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 31 |
937
+ | `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 44 |
938
+ | `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx` | 53 |
939
+ | `contracts.toasts.switchError` | "Failed to switch contract. Please try again." | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 42 |
940
+ | `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 95 |
941
+ | `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 100 |
942
+ | `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ContractListItem/ContractListItem.tsx` | 109 |
943
+ | `contracts.labels.selectAllCount` | "Select all ({0})" | `src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx` | 34 |
944
+ | `contracts.labels.selectedCount` | "{0} selected" | `src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx` | 32 |
945
+ | `shared.buttons.remove` | "Remove" | `src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx` | 42 |
946
+ | `contracts.listing.emptyState` | "No contracts found for this organizational unit." | `src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx` | 27 |
947
+ | `layouts.tabs.contract` | "Contract" | `src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx` | 29 |
948
+ | `layouts.titles.profile` | "Profile" | `src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx` | 32 |
949
+ | `contracts.listing.interval` | "{0} — {1} of {2}" | `src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx` | 35 |
950
+ | `contracts.removeDrawer.unableTitle` | "Unable to remove contracts" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 16 |
951
+ | `contracts.removeDrawer.error` | "An error occurred while removing the contract. Please try again." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 22 |
952
+ | `contracts.removeDrawer.minimumContractWarning` | "Every organizational unit must have at least one contract available..." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 27 |
953
+ | `contracts.removeDrawer.deselectHint` | "To proceed, deselect at least one contract to keep it in this organizational unit." | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 33 |
954
+ | `shared.buttons.cancel` | "Cancel" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 47 |
955
+ | `shared.buttons.tryAgain` | "Try again" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 54 |
956
+ | `shared.buttons.gotIt` | "Got it" | `src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx` | 63 |
957
+ | `contracts.removeDrawer.unableTitle` | "Unable to remove contracts" | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 23 |
958
+ | `contracts.removeDrawer.minimumContractWarning` | "Every organizational unit must have at least one contract available..." | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 31 |
959
+ | `contracts.removeDrawer.unableDeselectHint` | "To proceed, deselect at least one contract to keep it in this {0}..." | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 36 |
960
+ | `shared.buttons.gotIt` | "Got it" | `src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx` | 47 |
961
+ | `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 46 |
962
+ | `layouts.navigation.addresses` | "Addresses" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 52 |
963
+ | `layouts.navigation.paymentMethods` | "Payment methods" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 56 |
964
+ | `layouts.navigation.creditCards` | "Credit cards" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 61 |
965
+ | `contracts.nav.assortment` | "Assortment" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 66 |
966
+ | `layouts.navigation.accountingFields` | "Accounting fields" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 72 |
967
+ | `contracts.nav.costCenters` | "Cost centers" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 82 |
968
+ | `contracts.nav.departments` | "Departments" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 90 |
969
+ | `contracts.nav.poNumbers` | "PO numbers" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 100 |
970
+ | `contracts.toasts.setDefaultSuccess` | "Default contract updated successfully" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 168 |
971
+ | `contracts.toasts.setDefaultError` | "Failed to set default contract. Please try again." | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 182 |
972
+ | `layouts.navigation.contracts` | "Contracts" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 217 |
973
+ | `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 235 |
974
+ | `contracts.actions.setAsDefault` | "Set as default" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 243 |
975
+ | `shared.buttons.remove` | "Remove" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 251 |
976
+ | `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 267 |
977
+ | `shared.labels.name` | "Name" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 271 |
978
+ | `shared.labels.email` | "Email" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 277 |
979
+ | `contracts.details.createdDate` | "Created date" | `src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx` | 283 |
980
+ | `contracts.listing.infoTooltip` | "Contracts define the commercial rules applied during purchasing..." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 59 |
981
+ | `contracts.toasts.refreshError` | "Contracts removed, but we couldn't refresh your active contract. Please reload the page." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 149 |
982
+ | `contracts.toasts.removeSuccess` | "{0} contract{1} removed successfully" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 162 |
983
+ | `contracts.toasts.removeError` | "Failed to remove contracts. Please try again." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 178 |
984
+ | `contracts.toasts.setDefaultSuccess` | "Default contract updated successfully" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 209 |
985
+ | `contracts.toasts.setDefaultError` | "Failed to set default contract. Please try again." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 224 |
986
+ | `contracts.toasts.loadError` | "Failed to load contract information." | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 246 |
987
+ | `contracts.titles.contracts` | "Contracts" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 328 |
988
+ | `orgUnitDetails.links.manageContractSettings` | "Manage contract settings" | `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx` | 343 |
989
+ | `contracts.nav.basicInformation` | "Basic information" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 34 |
990
+ | `layouts.navigation.addresses` | "Addresses" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 38 |
991
+ | `layouts.navigation.paymentMethods` | "Payment methods" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 42 |
992
+ | `layouts.navigation.creditCards` | "Credit cards" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 46 |
993
+ | `contracts.nav.assortment` | "Assortment" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 50 |
994
+ | `layouts.navigation.accountingFields` | "Accounting fields" | `src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx` | 54 |
917
995
 
918
996
  ---
919
997
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,243 @@
1
+ # Analytics Actor Type (Internal vs External)
2
+
3
+ > **Status**: Done
4
+ > **Created**: 2026-07-28
5
+ > **Jira**: [B2BTEAM-3639](https://vtex-dev.atlassian.net/browse/B2BTEAM-3639)
6
+ > **Epic**: [B2BTEAM-3596](https://vtex-dev.atlassian.net/browse/B2BTEAM-3596) — Melhoria 1
7
+ > **Sibling task**: [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640) (Redshift column + QuickSight segmentation)
8
+
9
+ ## 1. Business Context
10
+
11
+ ### Problem Statement
12
+
13
+ Organization Account already emits usage analytics (create/edit/delete, timing, errors) via `useAnalytics` into the VTEX Data Ingestion API, Redshift, and QuickSight. Those numbers mix real customer usage with traffic from VTEX employees testing and supporting accounts.
14
+
15
+ PMs cannot filter “real usage” without a reliable, PII-free signal on each event that says whether the actor was internal VTEX or external.
16
+
17
+ ### Goals
18
+
19
+ 1. Every Org Account analytics event includes a derived `actor_type` of `internal` or `external`.
20
+ 2. Usage metrics can exclude VTEX traffic without dropping error analysis options (filtering stays at the consumer; emission always sends the field).
21
+ 3. Zero PII in the analytics envelope: never send email, login, user id, or other identity — only the derived enum.
22
+
23
+ ### User Stories
24
+
25
+ #### US-1: Emit actor_type on every analytics event
26
+
27
+ - **Story**: As a PM analyzing Org Account usage, I want every event tagged as internal or external, so I can segment real customer usage from VTEX traffic.
28
+ - **Acceptance Criteria**:
29
+ - **Given** a logged-in user whose email domain is `vtex.com`, **when** any analytics event is emitted through `useAnalytics` / `useDataIngestionApi`, **then** the payload includes `actor_type: "internal"`.
30
+ - **Given** a logged-in user whose email domain is not `vtex.com`, **when** any analytics event is emitted, **then** the payload includes `actor_type: "external"`.
31
+ - **Given** any successful emission path (`trackEvent`, `trackError`, `trackEntityCreated`, `trackEntityEdited`, `trackEntityCreateError`, `trackEntityEditError`), **when** the event is sent, **then** `actor_type` is present on the envelope (not only inside caller-supplied metadata).
32
+
33
+ #### US-2: Never send identity or email in the event
34
+
35
+ - **Story**: As a platform owner, I want the analytics envelope to stay free of PII, so we do not leak user identity into Redshift / QuickSight / Slack summaries.
36
+ - **Acceptance Criteria**:
37
+ - **Given** a user with email `alice@vtex.com`, **when** an event is emitted, **then** the JSON body does not contain that email, login, `userId`, or session `sub`.
38
+ - **Given** derivation needs the email in memory, **when** building the payload, **then** only the enum `actor_type` is attached to the outgoing event.
39
+
40
+ #### US-3: Safe default when identity is unavailable
41
+
42
+ - **Story**: As an engineer, I want a deterministic fallback when email is missing, so every event still carries `actor_type` and the pipeline never breaks.
43
+ - **Acceptance Criteria**:
44
+ - **Given** `currentUser` is null or `email` is empty/missing, **when** an event is emitted, **then** `actor_type` is `"external"`.
45
+ - **Given** derivation throws or email is malformed (no `@`), **when** an event is emitted, **then** `actor_type` is `"external"` and emission still succeeds.
46
+
47
+ ### Key Scenarios
48
+
49
+ | Scenario | Pre-conditions | Steps | Expected Result |
50
+ |---|---|---|---|
51
+ | Happy path — VTEX employee | `currentUser.email` is `rodrigo.tavares@vtex.com` | User creates a budget; `trackEntityCreated` runs | Event has `actor_type: "internal"`; no email in payload |
52
+ | Happy path — customer | `currentUser.email` is `buyer@acme.com` | User edits an address; `trackEntityEdited` runs | Event has `actor_type: "external"` |
53
+ | Error case — Users API failed in loader | `currentUser` is `null` (e.g. `getUserByIdService` returned null) | User still triggers an analytics call from a mounted component | Event has `actor_type: "external"`; send does not throw |
54
+ | Edge case — username login, email from API | Session `sub` is `everton.ataidetestdev` (not an email); Users API returns `email: "everton@vtex.com"` | Any event is emitted | Classification uses API email → `internal`; session `sub` is never sent |
55
+ | Edge case — case / plus addressing | Email is `Name+qa@VTEX.COM` | Event emitted | Domain match is case-insensitive → `internal` |
56
+
57
+ ### Functional Requirements
58
+
59
+ 1. Derive `actor_type` at emission time from the logged-in user's email domain.
60
+ 2. Classification rule: domain equals `vtex.com` (case-insensitive) → `internal`; otherwise → `external`.
61
+ 3. Attach `actor_type` on every event sent through the shared analytics send path so callers do not need to pass it.
62
+ 4. Do not attach email, login, userId, session `sub`, or other identity fields to the event.
63
+ 5. Remain backward compatible with the existing schemaless ingestion pipeline (additive field only).
64
+
65
+ ### Non-Functional Requirements
66
+
67
+ 1. **Privacy**: envelope stays zero-PII for identity; derivation is local/ephemeral.
68
+ 2. **Reliability**: missing user context must not block or fail analytics emission.
69
+ 3. **Performance**: derivation is O(1) string work; no extra network calls at emit time.
70
+ 4. **Testability**: pure domain classifier is unit-tested; send path asserts field presence and absence of PII.
71
+
72
+ ### Out of Scope
73
+
74
+ - Exposing `actor_type` as a Redshift column or QuickSight filter (sibling [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640)).
75
+ - Melhoria 2 (`error_type` / stripping raw `error_message` / `error_stack`).
76
+ - Rewriting the telemetry pipeline, DAG, or Slack weekly summary.
77
+ - Classifying by session `sub` alone, account name, IP, or workspace.
78
+ - Matching sibling domains (e.g. `vtex.com.br`) or subdomains (e.g. `corp.vtex.com`) — only exact `vtex.com`.
79
+
80
+ ---
81
+
82
+ ## 2. Arch Decisions
83
+
84
+ ### Proposed Solution
85
+
86
+ Enrich the shared analytics send path so every event automatically receives `actor_type`, derived from `currentUser.email` already available in `BuyerPortalContext`.
87
+
88
+ **Identity source (important):** the auth session JWT does **not** carry email. Example session claims:
89
+
90
+ ```json
91
+ {
92
+ "sub": "everton.ataidetestdev",
93
+ "userId": "dd801513-ae2f-4ff8-b6de-dd3a2159a895",
94
+ "customerId": "…",
95
+ "unitId": "…",
96
+ "account": "b2bfaststoredev",
97
+ "type": "user"
98
+ }
99
+ ```
100
+
101
+ `sub` is a login/username, not an email. Email is loaded server-side in page loaders via `getUserByIdService` → Users API (`usersClient.getUserById`), then passed into `BuyerPortalProvider` as `currentUser.email`. Classification must use that email, never the session `sub`.
102
+
103
+ ```mermaid
104
+ flowchart LR
105
+ Cookie["Auth cookie JWT\nuserId, sub, …"] --> Loader["Page loader\nwithAuthLoader"]
106
+ Loader --> UsersAPI["getUserByIdService\nUsers API"]
107
+ UsersAPI --> Ctx["BuyerPortalContext\ncurrentUser.email"]
108
+ Ctx --> Derive["deriveActorType(email)\ninternal | external"]
109
+ Derive --> Send["useDataIngestionApi.sendEvent\n+ actor_type"]
110
+ Send --> Ingest["VTEX schemaless-events"]
111
+ ```
112
+
113
+ ### Architecture Overview
114
+
115
+ | Layer | Responsibility |
116
+ |---|---|
117
+ | `deriveActorType(email?)` | Pure helper: parse domain, return `internal` \| `external` |
118
+ | `useDataIngestionApi` | Read `currentUser?.email` from `BuyerPortalContext` (optional context, like `useLogger`); merge `actor_type` into every `sendEvent` payload at top level alongside `account`, `locale`, `device`, `production` |
119
+ | `useAnalytics` | Unchanged call sites; inherits enrichment via `sendEvent` |
120
+ | Callers | No changes required to pass `actor_type` |
121
+
122
+ ### Alternatives Considered
123
+
124
+ | Alternative | Pros | Cons | Verdict |
125
+ |---|---|---|---|
126
+ | Derive in each feature caller | Explicit per event | Easy to miss; duplicates logic | Rejected |
127
+ | Derive only inside `useAnalytics.withDefaults` | Covers most UI tracking | Anything calling `useDataIngestionApi` / `dataIngestionApi` directly could skip it; harder to guarantee “every event” | Rejected as sole approach |
128
+ | Classify from session `sub` | No dependency on Users API | `sub` is often not an email (e.g. `everton.ataidetestdev`); false `external` for VTEX staff | Rejected |
129
+ | Put `actor_type` only inside `metadata` | Minimal type change | Sibling Redshift task wants a first-class column; top-level matches existing common fields | Rejected |
130
+ | Centralize in `useDataIngestionApi` + pure helper | Single choke point; additive; testable | Hook must tolerate missing React context | **Accepted** |
131
+
132
+ ### Risks & Mitigations
133
+
134
+ | Risk | Impact | Likelihood | Mitigation |
135
+ |---|---|---|---|
136
+ | `currentUser` null when analytics fires | Med (VTEX traffic mislabeled as external) | Med | Default `external`; ensure loaders keep loading user where events fire; document gap |
137
+ | VTEX staff using non-`@vtex.com` email | Med (internal traffic still pollutes usage) | Low | Document rule; extend domain list only via explicit follow-up |
138
+ | Customer email on `vtex.com` (unlikely) | Low | Low | Accept false `internal`; rule is domain-based by product decision |
139
+ | Accidental PII leak via spread of user object | High | Low | Unit tests asserting outbound payload keys; helper never returns email |
140
+
141
+ ### Key Decisions
142
+
143
+ #### Decision 1: Classification rule = exact `@vtex.com` domain
144
+
145
+ - **Status**: Accepted
146
+ - **Context**: Need a simple, privacy-safe heuristic for “VTEX employee”.
147
+ - **Decision**: `internal` iff the email’s domain equals `vtex.com` (case-insensitive). Plus-tags (`user+tag@vtex.com`) still match. Domains like `vtex.com.br` or `*.vtex.com` are **not** internal in this iteration.
148
+ - **Consequences**: Simple to implement and document; may miss regional VTEX domains until explicitly expanded.
149
+
150
+ #### Decision 2: Fallback = `external` when email unavailable
151
+
152
+ - **Status**: Accepted
153
+ - **Context**: Ticket enum is only `internal \| external`; every event must carry the field.
154
+ - **Decision**: Missing/invalid email → `external`.
155
+ - **Consequences**: Some VTEX traffic without a loaded user profile remains in “external” usage until context is present; safer than inventing a third enum value for Melhoria 1.
156
+
157
+ #### Decision 3: Identity source = `currentUser.email` from BuyerPortalContext
158
+
159
+ - **Status**: Accepted
160
+ - **Context**: Session JWT has `userId` / `sub` but not email; email already exists on `UserData` after loader fetch.
161
+ - **Decision**: Read `currentUser?.email` from context at send time. Do not decode the auth cookie on the client for classification. Do not use `login` / `sub` unless it is later proven to be an email and product asks for it — out of scope for v1 (email-only).
162
+ - **Consequences**: Correct for the common path where pages load the user; no extra network at emit time.
163
+
164
+ #### Decision 4: Place `actor_type` at top level of the event envelope
165
+
166
+ - **Status**: Accepted
167
+ - **Context**: Sibling task needs a Redshift column; common context fields (`account`, `locale`, `device`, `production`) are already top-level.
168
+ - **Decision**: Set `actor_type` as a top-level field on the object passed to `dataIngestionApi`, not buried only under `metadata`.
169
+ - **Consequences**: Easier DAG mapping; additive and backward compatible for schemaless ingestion.
170
+
171
+ ### Implementation Plan
172
+
173
+ 1. Add `ActorType` type and pure `deriveActorType(email?: string | null): ActorType` under shared analytics (or shared utils) with unit tests.
174
+ 2. Extend `DataIngestionApiFields` / send path types with optional/required `actor_type`.
175
+ 3. Update `useDataIngestionApi` to read `BuyerPortalContext` (gracefully if undefined) and always set `actor_type` on `sendEvent`.
176
+ 4. Add tests that mock context email and assert outbound payload contains `actor_type` and does not contain email/login/userId.
177
+ 5. Manual smoke on a host store: one `@vtex.com` user and one external user; confirm events in the analytics pipeline / network tab.
178
+ 6. Hand off to [B2BTEAM-3640](https://vtex-dev.atlassian.net/browse/B2BTEAM-3640) for Redshift + QuickSight.
179
+
180
+ ---
181
+
182
+ ## 3. Technical Contract
183
+
184
+ ### Data Models
185
+
186
+ ```ts
187
+ type ActorType = "internal" | "external";
188
+
189
+ /** Pure classifier — never logs or returns the input email. */
190
+ function deriveActorType(email?: string | null): ActorType;
191
+ ```
192
+
193
+ **Rules for `deriveActorType`:**
194
+
195
+ 1. Trim and lowercase the email.
196
+ 2. If empty or no `@`, return `"external"`.
197
+ 3. Take the substring after the last `@` as the domain.
198
+ 4. If domain === `"vtex.com"`, return `"internal"`; else `"external"`.
199
+
200
+ **Event envelope (additive):**
201
+
202
+ | Field | Type | Required on emit | Notes |
203
+ |---|---|---|---|
204
+ | `actor_type` | `"internal" \| "external"` | Yes | Derived; top-level |
205
+ | `email` / `login` / `userId` / `sub` | — | **Must not appear** | PII / identity ban |
206
+
207
+ Existing fields (`name`, `account`, `locale`, `production`, `device`, `event_name`, `event_category`, `metadata`, …) unchanged.
208
+
209
+ ### Interfaces
210
+
211
+ ```ts
212
+ // analytics types — additive
213
+ interface DataIngestionApiFields {
214
+ // …existing fields…
215
+ actor_type?: ActorType; // set by send path; callers need not pass
216
+ }
217
+
218
+ // useDataIngestionApi sendEvent behavior
219
+ sendEvent(fields: DataIngestionApiFields): void
220
+ // always merges: { ...commonFields, ...fields, actor_type: deriveActorType(currentUser?.email) }
221
+ // caller-supplied actor_type must not override the derived value (server of truth = derivation)
222
+ ```
223
+
224
+ `useAnalytics` public API stays the same; no new required options on `UseAnalyticsConfig`.
225
+
226
+ ### Integration Points
227
+
228
+ | System | Interaction |
229
+ |---|---|
230
+ | Auth cookie / session JWT | Provides `userId` only (indirectly); **not** used for domain classification |
231
+ | Users API via `getUserByIdService` | Source of `currentUser.email` in loaders |
232
+ | `BuyerPortalContext` | Runtime source of email for derivation in the client send hook |
233
+ | VTEX Data Ingestion (`schemaless-events`) | Receives additive `actor_type`; no schema migration in this task |
234
+ | Redshift / QuickSight | Consumer of the field in sibling task B2BTEAM-3640 |
235
+
236
+ ### Invariants & Constraints
237
+
238
+ 1. Every event leaving `useDataIngestionApi.sendEvent` includes `actor_type`.
239
+ 2. Outgoing analytics JSON must never include email, login, userId, or session `sub` as a result of this feature.
240
+ 3. Derivation does not perform network I/O.
241
+ 4. Callers cannot force a conflicting `actor_type` that bypasses derivation (derived value wins).
242
+ 5. Domains other than exact `vtex.com` are always `external` in this version.
243
+ 6. This feature does not change event names, categories, or existing metadata shapes beyond the additive field.
@@ -8,6 +8,7 @@ import {
8
8
  Icon,
9
9
  LetterHighlight,
10
10
  } from "../../../shared/components";
11
+ import { useLocalization } from "../../../shared/localization/LocalizationContext";
11
12
  import { useAddContracts, useListAvailableContracts } from "../../hooks";
12
13
  import {
13
14
  buildRangeSet,
@@ -30,6 +31,7 @@ export const AddContractsDrawer = ({
30
31
  onSuccess,
31
32
  ...drawerProps
32
33
  }: AddContractsDrawerProps) => {
34
+ const { t } = useLocalization();
33
35
  const { pushToast } = useUI();
34
36
 
35
37
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -50,7 +52,7 @@ export const AddContractsDrawer = ({
50
52
  onError: () => {
51
53
  drawerProps.onDismiss?.();
52
54
  pushToast({
53
- message: "Failed to add contracts. Please try again.",
55
+ message: t("contracts.toasts.addError"),
54
56
  status: "ERROR",
55
57
  });
56
58
  },
@@ -142,23 +144,24 @@ export const AddContractsDrawer = ({
142
144
  return (
143
145
  <BasicDrawer size="large" {...drawerProps}>
144
146
  <BasicDrawer.Heading
145
- title="Add contracts"
147
+ title={t("contracts.addDrawer.title")}
146
148
  onClose={isAddContractsLoading ? undefined : drawerProps.onDismiss}
147
149
  />
148
150
 
149
151
  <BasicDrawer.Body data-fs-bp-add-contracts-drawer-body>
150
152
  <p data-fs-bp-add-contracts-drawer-description>
151
- Add available contracts to{" "}
152
- <strong data-fs-bp-add-contracts-drawer-unit-name>
153
- {orgUnitName}
154
- </strong>
153
+ {t("contracts.addDrawer.description", [
154
+ <strong data-fs-bp-add-contracts-drawer-unit-name key="orgUnitName">
155
+ {orgUnitName}
156
+ </strong>,
157
+ ])}
155
158
  </p>
156
159
 
157
160
  <div data-fs-bp-add-contracts-drawer-toolbar>
158
161
  <div data-fs-bp-add-contracts-drawer-search>
159
162
  <input
160
163
  type="text"
161
- placeholder="Search"
164
+ placeholder={t("shared.placeholders.search")}
162
165
  value={searchQuery}
163
166
  onChange={handleSearchChange}
164
167
  disabled={isDisabled}
@@ -187,7 +190,11 @@ export const AddContractsDrawer = ({
187
190
  aria-label="Pagination"
188
191
  >
189
192
  <span data-fs-bp-add-contracts-drawer-pagination-range>
190
- {rangeStart}–{rangeEnd} of {filteredContracts.length}
193
+ {t("contracts.addDrawer.paginationRange", [
194
+ rangeStart,
195
+ rangeEnd,
196
+ filteredContracts.length,
197
+ ])}
191
198
  </span>
192
199
  <button
193
200
  type="button"
@@ -215,14 +222,14 @@ export const AddContractsDrawer = ({
215
222
 
216
223
  {availableContractsLoading ? (
217
224
  <div data-fs-bp-add-contracts-drawer-loading aria-busy="true">
218
- Loading contracts
225
+ {t("contracts.addDrawer.loading")}
219
226
  </div>
220
227
  ) : filteredContracts.length === 0 ? (
221
228
  <div data-fs-bp-add-contracts-drawer-empty>
222
229
  <p>
223
230
  {searchQuery
224
- ? "No contracts match your search."
225
- : "No contracts available to add."}
231
+ ? t("contracts.addDrawer.noSearchResults")
232
+ : t("contracts.addDrawer.noneAvailable")}
226
233
  </p>
227
234
  </div>
228
235
  ) : (
@@ -248,7 +255,11 @@ export const AddContractsDrawer = ({
248
255
  <Icon name="Check" width={12} height={12} />
249
256
  )}
250
257
  </div>
251
- <span>Select all ({filteredContracts.length})</span>
258
+ <span>
259
+ {t("contracts.labels.selectAllCount", [
260
+ filteredContracts.length,
261
+ ])}
262
+ </span>
252
263
  </div>
253
264
 
254
265
  {displayedContracts.map((contract) => {
@@ -296,7 +307,9 @@ export const AddContractsDrawer = ({
296
307
  >
297
308
  <Icon name="X" width={14} height={14} />
298
309
  </button>
299
- <span>{selectedIds.size} selected</span>
310
+ <span>
311
+ {t("contracts.labels.selectedCount", [selectedIds.size])}
312
+ </span>
300
313
  </>
301
314
  )}
302
315
  </div>
@@ -306,7 +319,7 @@ export const AddContractsDrawer = ({
306
319
  type="button"
307
320
  disabled={isDisabled}
308
321
  >
309
- Cancel
322
+ {t("shared.buttons.cancel")}
310
323
  </BasicDrawer.Button>
311
324
  <BasicDrawer.Button
312
325
  variant="confirm"
@@ -315,7 +328,7 @@ export const AddContractsDrawer = ({
315
328
  disabled={selectedIds.size === 0 || isDisabled}
316
329
  isLoading={isAddContractsLoading}
317
330
  >
318
- Add
331
+ {t("shared.buttons.add")}
319
332
  </BasicDrawer.Button>
320
333
  </BasicDrawer.Footer>
321
334
  </BasicDrawer>
@@ -1,4 +1,5 @@
1
1
  import { BasicDrawer, type BasicDrawerProps } from "../../../shared/components";
2
+ import { useLocalization } from "../../../shared/localization/LocalizationContext";
2
3
 
3
4
  export type ConfirmRemoveDrawerProps = Omit<BasicDrawerProps, "children"> & {
4
5
  orgUnitName: string;
@@ -14,25 +15,27 @@ export const ConfirmRemoveDrawer = ({
14
15
  isLoading = false,
15
16
  ...drawerProps
16
17
  }: ConfirmRemoveDrawerProps) => {
17
- const label = count === 1 ? "contract" : "contracts";
18
+ const { t } = useLocalization();
19
+ const label =
20
+ count === 1
21
+ ? t("contracts.labels.contract")
22
+ : t("contracts.labels.contracts");
18
23
 
19
24
  return (
20
25
  <BasicDrawer {...drawerProps}>
21
26
  <BasicDrawer.Heading
22
- title={`Remove ${label}`}
27
+ title={t("contracts.removeDrawer.title", [label])}
23
28
  onClose={drawerProps.onDismiss}
24
29
  />
25
30
  <BasicDrawer.Body data-fs-bp-confirm-remove-drawer-body>
26
31
  <p>
27
- Are you sure you want to remove <strong>{count}</strong> {label} from{" "}
28
- <strong>{orgUnitName}</strong>?
29
- </p>
30
- <p>
31
- Removed contracts can be added again later, but any specific
32
- configurations for these contracts within this organizational unit
33
- will be lost. This will prevent users in this organization unit from
34
- accessing these contracts during store navigation and checkout.
32
+ {t("contracts.removeDrawer.question", [
33
+ <strong key="count">{count}</strong>,
34
+ label,
35
+ <strong key="orgUnitName">{orgUnitName}</strong>,
36
+ ])}
35
37
  </p>
38
+ <p>{t("contracts.removeDrawer.warning")}</p>
36
39
  </BasicDrawer.Body>
37
40
  <BasicDrawer.Footer>
38
41
  <BasicDrawer.Button
@@ -41,7 +44,7 @@ export const ConfirmRemoveDrawer = ({
41
44
  type="button"
42
45
  disabled={isLoading}
43
46
  >
44
- Cancel
47
+ {t("shared.buttons.cancel")}
45
48
  </BasicDrawer.Button>
46
49
  <BasicDrawer.Button
47
50
  variant="danger"
@@ -50,7 +53,7 @@ export const ConfirmRemoveDrawer = ({
50
53
  isLoading={isLoading}
51
54
  disabled={isLoading}
52
55
  >
53
- Remove
56
+ {t("shared.buttons.remove")}
54
57
  </BasicDrawer.Button>
55
58
  </BasicDrawer.Footer>
56
59
  </BasicDrawer>