@vtex/faststore-plugin-buyer-portal 2.0.17 → 2.0.19
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.
- package/CHANGELOG.md +15 -1
- package/package.json +3 -3
- package/specs/credit-card-saved-cards-migration.md +296 -0
- package/src/features/credit-cards/clients/SavedCardsClient.ts +78 -0
- package/src/features/credit-cards/clients/__tests__/SavedCardsClient.test.ts +120 -0
- package/src/features/credit-cards/components/CreditCardDropdownMenu/CreditCardDropdownMenu.tsx +2 -1
- package/src/features/credit-cards/layouts/CreditCardsLayout/CreditCardLayout.tsx +2 -1
- package/src/features/credit-cards/services/__tests__/delete-credit-card.service.test.ts +94 -0
- package/src/features/credit-cards/services/__tests__/get-credit-card-list.service.test.ts +158 -0
- package/src/features/credit-cards/services/__tests__/get-payment-session.service.test.ts +69 -0
- package/src/features/credit-cards/services/__tests__/save-credit-card.service.test.ts +106 -0
- package/src/features/credit-cards/services/delete-credit-card.service.ts +9 -7
- package/src/features/credit-cards/services/get-credit-card-list.service.ts +7 -6
- package/src/features/credit-cards/services/get-payment-session.service.ts +7 -6
- package/src/features/credit-cards/services/save-credit-card.service.ts +13 -8
- package/src/features/credit-cards/types/CreditCard.ts +25 -1
- package/src/features/credit-cards/types/index.ts +4 -0
- package/src/features/credit-cards/utils/__tests__/getCreditCardDisplayName.test.ts +32 -0
- package/src/features/credit-cards/utils/__tests__/getVtexSessionFromCookie.test.ts +22 -0
- package/src/features/credit-cards/utils/getCreditCardDisplayName.ts +13 -0
- package/src/features/credit-cards/utils/getVtexSessionFromCookie.ts +17 -0
- package/src/features/credit-cards/utils/index.ts +2 -0
- package/src/features/shared/utils/api.ts +8 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/shared/utils/index.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.19] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Upgrade `@faststore/ui` dependency to 4.5.0
|
|
14
|
+
- Upgrade `@faststore/core` dependency to 4.5.0
|
|
15
|
+
|
|
16
|
+
## [2.0.18] - 2026-08-03
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Credit card registration, listing, and deletion now call the new `saved-cards` microservice directly instead of the `buyer-portal-graphql` BFF (card tokenization is unchanged). Payment validators and the "set as default"/active-inactive toggle keep working against the BFF as before. Cards show their nickname when available, falling back to the masked card number otherwise — the same fallback the list already used
|
|
21
|
+
|
|
10
22
|
## [2.0.17] - 2026-07-30
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -809,7 +821,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
809
821
|
- Add CHANGELOG file
|
|
810
822
|
- Add README file
|
|
811
823
|
|
|
812
|
-
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.
|
|
824
|
+
[unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.19...HEAD
|
|
813
825
|
[1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
|
|
814
826
|
[1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
|
|
815
827
|
[1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
|
|
@@ -907,6 +919,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
907
919
|
[2.0.10]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.9...v2.0.10
|
|
908
920
|
[2.0.9]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/2.0.9
|
|
909
921
|
|
|
922
|
+
[2.0.19]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.18...v2.0.19
|
|
923
|
+
[2.0.18]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.17...v2.0.18
|
|
910
924
|
[2.0.17]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.16...v2.0.17
|
|
911
925
|
[2.0.16]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.15...v2.0.16
|
|
912
926
|
[2.0.15]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v2.0.14...v2.0.15
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtex/faststore-plugin-buyer-portal",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "A plugin for faststore with buyer portal",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@eslint/js": "^9.29.0",
|
|
25
|
-
"@faststore/core": "
|
|
26
|
-
"@faststore/ui": "
|
|
25
|
+
"@faststore/core": "4.5.0",
|
|
26
|
+
"@faststore/ui": "4.5.0",
|
|
27
27
|
"@release-it/bumper": "^4.0.0",
|
|
28
28
|
"@release-it/keep-a-changelog": "^3.0.0",
|
|
29
29
|
"@types/react": "^18.2.42",
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Credit Card Registration — Migrate BFF Calls to the Saved-Cards Service
|
|
2
|
+
|
|
3
|
+
> **Status**: Approved (implementation in this PR is incomplete — see Decision 7; `register`/`delete` are confirmed non-functional as written, pending a CORS fix)
|
|
4
|
+
> **Created**: 2026-07-27
|
|
5
|
+
> **Jira**: [B2BTEAM-3418](https://vtex-dev.atlassian.net/browse/B2BTEAM-3418) — "[Organization Account] Alterar o fluxo de cadastro de cartão para usar a nova api"
|
|
6
|
+
> **Scope note**: The Jira ticket's literal wording ("card-registration flow") is expanded here to cover every credit-card operation the new backend actually supports today — session, register, list, delete — confirmed with the assignee. Operations the new backend does **not** expose (validators, default/active toggling) stay on the BFF; see [Out of Scope](#out-of-scope).
|
|
7
|
+
> **Backend distinction**: an earlier candidate backend, `vtex/personal-cards-service` (.NET), was investigated and found decommissioned — deprecated in its catalog entry, CI/CD disabled, no controllers beyond a healthcheck. The live backend is **`vtex/saved-cards`** (Postman collection `43115468-febaf668-f6e8-4275-9991-8199bddb47e9`, contract at `.vtex/definitions/*.tsp`, confirmed 2026-07-27 by reading the service's source, tests, and TypeSpec contract directly). Do not wire against `personal-cards-service`.
|
|
8
|
+
|
|
9
|
+
## 1. Business Context
|
|
10
|
+
|
|
11
|
+
### Problem Statement
|
|
12
|
+
|
|
13
|
+
Today, `faststore-plugin-buyer-portal`'s credit-card feature (`src/features/credit-cards`) talks to the `buyer-portal-graphql` BFF for everything except card tokenization: getting a payment session, listing cards, registering a card, and deleting a card all go through `CreditCardClient` → the BFF. VTEX has since built a dedicated microservice for this, `saved-cards`, which owns the card-registration business logic (charge orchestration, 3DS, Profile persistence) that used to live in the BFF/legacy stack. Card data must keep flowing through the same PCI-compliant path — tokenization stays in the Organization Account, calling `vtexpayments.com.br` directly — but everything downstream of tokenization should call the new service instead of the BFF.
|
|
14
|
+
|
|
15
|
+
### Goals
|
|
16
|
+
|
|
17
|
+
- Every card operation the `saved-cards` service supports today (session, register, list, delete) is served by `saved-cards`, not the BFF.
|
|
18
|
+
- The PCI tokenization step is untouched — no card PAN/CVV data is newly introduced into any call to `saved-cards`.
|
|
19
|
+
- No functional regression for the buyer: register/list/delete continue to work exactly as observed today. Card nicknames (`cardLabel`) render whenever `saved-cards` provides the field, falling back to the masked card number otherwise.
|
|
20
|
+
- Operations `saved-cards` does not expose yet (validators, default/active toggling) keep working unchanged on the BFF — this migration does not remove or break them.
|
|
21
|
+
|
|
22
|
+
### User Stories
|
|
23
|
+
|
|
24
|
+
#### US-1: Buyer registers a new credit card
|
|
25
|
+
|
|
26
|
+
- **Story**: As a B2B buyer, I want to register a credit card for my organization, so that I can use it for future purchases.
|
|
27
|
+
- **Acceptance Criteria**:
|
|
28
|
+
- **Given** a buyer fills out the card form and confirms, **when** the app requests a payment session, **then** it calls `GET /api/saved-cards/payments/session` (not the BFF) and receives a session id.
|
|
29
|
+
- **Given** a valid payment session and a successfully tokenized card, **when** the app submits the card, **then** it calls `POST /api/saved-cards/credit-card` (not the BFF) with `{ token: { creditCardToken, paymentSystem }, recaptchaV3Token }`.
|
|
30
|
+
- **Given** the register call succeeds, **when** the response returns, **then** the buyer sees the same success toast and page reload as today (the response body is not otherwise consumed by the UI).
|
|
31
|
+
- **Given** the register call fails with any mapped error (`400`, `422`, `500`, `502`), **when** the failure occurs, **then** the buyer sees the existing generic "couldn't save card" error toast — no new per-error-code UI is introduced by this change.
|
|
32
|
+
|
|
33
|
+
#### US-2: Buyer sees their registered cards
|
|
34
|
+
|
|
35
|
+
- **Story**: As a B2B buyer, I want to see the list of cards registered to my organizational unit, so that I can manage them.
|
|
36
|
+
- **Acceptance Criteria**:
|
|
37
|
+
- **Given** a buyer opens the credit cards page, **when** the list loads, **then** it calls `GET /api/saved-cards/credit-card` (not the BFF).
|
|
38
|
+
- **Given** a card has a nickname, **when** it renders in the list, **then** the nickname is shown; **given** a card was saved without one, **then** it falls back to the masked card number, exactly as the existing `creditCard.cardLabel ?? creditCard.cardNumber` fallback already does today.
|
|
39
|
+
- **Given** the buyer has no Organizational Unit association, **when** the list loads, **then** `isDefault`/`isActive` still resolve correctly (the service treats unit as optional).
|
|
40
|
+
|
|
41
|
+
#### US-3: Buyer deletes a card
|
|
42
|
+
|
|
43
|
+
- **Story**: As a B2B buyer, I want to delete a card I no longer use, so that it stops appearing as an option.
|
|
44
|
+
- **Acceptance Criteria**:
|
|
45
|
+
- **Given** a buyer confirms card deletion, **when** the app submits the deletion, **then** it calls `DELETE /api/saved-cards/credit-card/{tokenId}` (not the BFF), using the card's `accountId` as `tokenId` — the same identifier the UI already passes today.
|
|
46
|
+
- **Given** the deletion succeeds, **when** the response returns (`204`), **then** the buyer sees the existing success toast and page reload.
|
|
47
|
+
- **Given** the card no longer exists, **when** the deletion is attempted, **then** the service returns `404` and the buyer sees the existing generic error toast.
|
|
48
|
+
|
|
49
|
+
#### US-4: Saved-cards is unavailable or rejects the request
|
|
50
|
+
|
|
51
|
+
- **Story**: As a buyer, if the saved-cards backend is down or rejects my request, I want the same clear error behavior the app already has, so a broken migration doesn't produce a worse experience than today.
|
|
52
|
+
- **Acceptance Criteria**:
|
|
53
|
+
- **Given** `saved-cards` returns `502` (`DownstreamError`) on any of the four operations, **when** the call fails, **then** the existing error-handling path (`onError` on the corresponding hook) fires the same generic error toast used today.
|
|
54
|
+
- **Given** the register call is missing the required session header, **when** `saved-cards` returns `400 SESSION_REQUIRED`, **then** the failure is treated as a register error like any other (generic toast) — this is a signal for us to check header wiring, not a new buyer-facing state.
|
|
55
|
+
|
|
56
|
+
### Key Scenarios
|
|
57
|
+
|
|
58
|
+
| Scenario | Pre-conditions | Steps | Expected Result |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| Happy path — register a card | Buyer has a valid session, address, and tokenized card | Fill form → confirm → tokenize → register | Card is saved via `saved-cards`; success toast; list reflects the new card on next load |
|
|
61
|
+
| Error case — saved-cards down | `saved-cards` returns `502` on register | Buyer confirms the form | Existing generic error toast; loading state cleared; no partial/duplicate submission |
|
|
62
|
+
| Edge case — buyer with no Organizational Unit | Buyer account has no unit association | Buyer opens the cards list | List loads successfully; `isDefault`/`isActive` computed without unit-scoped data, matching current BFF behavior for unaffiliated buyers |
|
|
63
|
+
| Edge case — card has no label | Card was saved without a nickname (field omitted by `saved-cards`) | Buyer opens the cards list | Card renders using the masked card number fallback (no crash, no blank label) |
|
|
64
|
+
|
|
65
|
+
### Functional Requirements
|
|
66
|
+
|
|
67
|
+
- **FR-1**: `getPaymentSessionService` calls `GET /api/saved-cards/payments/session?an={account}` instead of the BFF equivalent.
|
|
68
|
+
- **FR-2**: `saveCreditCardService` (register) calls `POST /api/saved-cards/credit-card?an={account}` instead of the BFF equivalent, with the request body unchanged (`{ token: { creditCardToken, paymentSystem }, recaptchaV3Token }`).
|
|
69
|
+
- **FR-3**: `getCreditCardsListService` calls `GET /api/saved-cards/credit-card?an={account}` instead of the BFF equivalent.
|
|
70
|
+
- **FR-4**: `deleteCreditCardService` calls `DELETE /api/saved-cards/credit-card/{tokenId}?an={account}` instead of the BFF equivalent, sending the identifier as a path parameter instead of a request body.
|
|
71
|
+
- **FR-5**: The tokenization call (`TokenClient` → `vtexpayments.com.br`) is unchanged — this migration does not touch how or where card data is tokenized.
|
|
72
|
+
- **FR-6**: Requests to `saved-cards` carry the buyer's `VtexIdclientAutCookie` cookie (as today) plus a dedicated `Vtex_session` HTTP header carrying the raw `vtex_session` cookie value — required specifically because the register endpoint (`POST /credit-card`) rejects requests missing it (`400 SESSION_REQUIRED`).
|
|
73
|
+
- **FR-7**: `getPaymentValidators` (validators) and `setDefaultCreditCard`/`removeDefaultCreditCard` (default/active toggling) keep calling the BFF unchanged — see [Out of Scope](#out-of-scope).
|
|
74
|
+
- **FR-8**: No new per-error-code UI is introduced; all four migrated operations keep using their existing generic success/error toast handling.
|
|
75
|
+
|
|
76
|
+
### Non-Functional Requirements
|
|
77
|
+
|
|
78
|
+
- **No card data (PAN, CVV, expiry) is ever sent to `saved-cards`** — only the opaque token produced by tokenization, matching the existing PCI boundary.
|
|
79
|
+
- **No PII or auth material (cookies, tokens, `Vtex_session` values) is written to logs**, consistent with the project's existing safety guardrails.
|
|
80
|
+
- **Card nicknames (`cardLabel`) are handled as an optional field**: the UI renders the nickname when `saved-cards` returns it and falls back to the masked card number when it doesn't, so the frontend is correct either way. An earlier revision of this spec documented losing `cardLabel` as an accepted regression, based on the service contract at the time; `saved-cards` is reported to have since added the field, so the frontend was made forward-compatible with it. **Not yet observable**: as of 2026-07-30 the field is absent from every card returned by both `vtexcommercebeta` (`saved-cards-v0.0.8-stg-126`) and `vtexcommercestable` (`saved-cards-v0.0.7-prd-005`) for account `b2bfaststoredev` — verify against a deployment that actually returns it before treating nickname display as confirmed. The separate "rename card" drawer (`EditCreditCardDrawer`) still doesn't persist to any backend — already true before this migration, unchanged by it.
|
|
81
|
+
- The `Vtex_session` header change (FR-6) must be scoped to the new `saved-cards`-facing client only — the shared cookie utility (`getCookieWithoutSessionObjects`) that strips `vtex_session` for other calls must not be changed globally, to avoid affecting BFF calls that still expect that stripping behavior.
|
|
82
|
+
|
|
83
|
+
### Out of Scope
|
|
84
|
+
|
|
85
|
+
- **Payment validators** (`getPaymentValidators` — card brand regex/mask rules). `saved-cards` does not expose this endpoint today: it is present in the team's Postman collection but absent from the service's TypeSpec contract, controllers, and tests. This call stays on the BFF until (and unless) the endpoint is actually implemented server-side; migrating it is a separate, backend-first piece of work.
|
|
86
|
+
- **Default/active card toggling** (`DefaultValuesClient.setDefaultCreditCard` / `removeDefaultCreditCard`, used by the "set as default" and active/inactive toggle in `CreditCardLayout`). `saved-cards` only uses this data internally (via its own MasterData client) to compute the `isDefault`/`isActive` fields it returns from `list` — it does not expose a public endpoint to change them. Stays on the BFF.
|
|
87
|
+
- **Card rename / label editing.** Already a no-op today (the `EditCreditCardDrawer` doesn't call any persistence API); not something this migration needs to preserve or recreate.
|
|
88
|
+
- **Shared/organization-level card visibility** (a "Shared" or org-wide cards view). Out of scope for Organization Account entirely — that concept is being built as a separate My Account feature in `vtex/faststore` (Jira B2BTEAM-3636/3634), against the same `saved-cards` backend but in a different repo and PR.
|
|
89
|
+
- **Any UI/visual redesign** of the credit-card screens. This is a backend-integration swap, not a UX change.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 2. Arch Decisions
|
|
94
|
+
|
|
95
|
+
### Proposed Solution
|
|
96
|
+
|
|
97
|
+
Introduce a new client (working name: `SavedCardsClient`) in `src/features/credit-cards/clients/`, alongside the existing `TokenClient` (untouched) and `CreditCardClient`. The four service functions currently backed by `CreditCardClient` (`getPaymentSessionService`, `saveCreditCardService`, `getCreditCardsListService`, `deleteCreditCardService`) are repointed to the new client; `getCreditCardValidatorsService` and the default-value services keep using `CreditCardClient`/`DefaultValuesClient` against the BFF, unchanged. Hooks, components, and types keep their existing shapes wherever possible — the main type-level change is that `CreditCardData` (from `list`) treats `cardLabel` as optional, which the UI already tolerates via its existing fallback.
|
|
98
|
+
|
|
99
|
+
Authentication for the new client forwards the existing `VtexIdclientAutCookie` cookie (as today) plus a new, narrowly-scoped `Vtex_session` header sourced from the raw `vtex_session` browser cookie — read explicitly for this client, not through the shared `getCookieWithoutSessionObjects` utility (which strips it for other purposes and must keep doing so for BFF calls).
|
|
100
|
+
|
|
101
|
+
The base URL (`https://{account}.vtexcommercestable.com.br/api/saved-cards`) and the `Authorization: Bearer` question (not required) were both verified empirically against the live production service — see Decisions 4 and 5.
|
|
102
|
+
|
|
103
|
+
### Architecture Overview
|
|
104
|
+
|
|
105
|
+
```mermaid
|
|
106
|
+
sequenceDiagram
|
|
107
|
+
participant Buyer
|
|
108
|
+
participant UI as CreateCreditCardDrawer
|
|
109
|
+
participant SavedCards as saved-cards service
|
|
110
|
+
participant VtexPayments as vtexpayments.com.br (tokenization, PCI)
|
|
111
|
+
participant BFF as buyer-portal-graphql (BFF)
|
|
112
|
+
|
|
113
|
+
Buyer->>UI: Fill card form, confirm
|
|
114
|
+
UI->>SavedCards: GET /api/saved-cards/payments/session
|
|
115
|
+
SavedCards-->>UI: { id, name, expiresAt }
|
|
116
|
+
UI->>VtexPayments: POST {session}/tokens (card data — unchanged, PCI boundary)
|
|
117
|
+
VtexPayments-->>UI: { token, bin, lastDigits, ... }
|
|
118
|
+
UI->>SavedCards: POST /api/saved-cards/credit-card { token, recaptchaV3Token }
|
|
119
|
+
SavedCards-->>UI: 200 AddCardResponse / 4xx-5xx typed error
|
|
120
|
+
UI-->>Buyer: success toast + reload, or generic error toast
|
|
121
|
+
|
|
122
|
+
Note over UI,BFF: Validators and default/active toggling still call the BFF (out of scope)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Alternatives Considered
|
|
126
|
+
|
|
127
|
+
| Alternative | Pros | Cons | Verdict |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| Migrate only `registerCreditCard`, leave session/list/delete on the BFF | Smallest possible change; matches the Jira ticket's literal wording | Leaves the feature split across two backends for operations that already have a working `saved-cards` endpoint; more total migration work later | Rejected — the requester explicitly asked to migrate every route the new service already supports |
|
|
130
|
+
| Migrate everything including validators and default-value toggling | One clean cutover for the whole feature area | **Not technically possible today** — those endpoints don't exist in `saved-cards` (confirmed by reading its TypeSpec contract, controllers, and tests) | Rejected — would require inventing behavior against a nonexistent API |
|
|
131
|
+
| Repoint the existing `CreditCardClient` in place (same class, new base URL) instead of adding a new client | Fewer new files | Conflates two different backends/auth models (path-based `customerId`/`unitId` for the BFF vs auth-derived identity + `Vtex_session` header for `saved-cards`) behind one class, increasing the chance of leaking BFF-only assumptions into `saved-cards` calls (and vice versa) | Rejected — a dedicated client keeps the two backends' auth/URL conventions from bleeding into each other |
|
|
132
|
+
|
|
133
|
+
### Risks & Mitigations
|
|
134
|
+
|
|
135
|
+
| Risk | Impact | Likelihood | Mitigation |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| **`saved-cards` sends no CORS headers, so every client-side (browser) call to it fails in every environment — this is not a hypothetical, it was reproduced live.** `register` and `delete` are both wired as direct browser-side calls (`CreateCreditCardDrawer`/`DeleteCreditCardDrawer` handlers) and both are confirmed broken (see Decision 7). Unit tests don't catch this because they mock `fetch` and never exercise real CORS enforcement | **Critical** — two of the four migrated operations don't work end-to-end as currently implemented, in production exactly as much as locally | **Confirmed** (not a probability — already reproduced) | Do not ship this PR until Decision 7 is resolved: either get CORS support added to `saved-cards`, or move `register`/`delete` to a same-origin server-side proxy route in this plugin. Re-verify both operations live afterward — a passing unit test suite is not sufficient evidence (see Decision 6) |
|
|
138
|
+
| `register` (add card) has never been exercised to a real charge outcome (`200`/`400`/`422`) — blocked first by the explicit choice to avoid a real Gateway/3DS attempt, and now by the CORS issue above regardless | Medium (an edge case specific to the charge/3DS/Profile-persistence path could still surface only in `register`, on top of the CORS fix) | Low-Medium | Re-attempt after Decision 7's fix lands, with a real buyer session |
|
|
139
|
+
| `UseAdHocCard` permission enforcement is ambiguous — the TypeSpec contract sets `skipAuthorization: true` (skips the generic License-Manager-based check), but service docs describe automatic permission enforcement via the same decorator | Medium (a buyer without the role could be wrongly allowed or wrongly blocked) | Low-Medium | Verify behavior in staging with a buyer account that lacks `UseAdHocCard` before rollout; do not assume either behavior from the docs alone |
|
|
140
|
+
| Scoping the new `Vtex_session` header narrowly to the new client, but implemented carelessly, could leak into or break other BFF calls sharing `cookie.ts` | Medium | Low | Keep the header-sourcing logic local to the new client; do not modify `getCookieWithoutSessionObjects`'s existing stripping behavior |
|
|
141
|
+
|
|
142
|
+
### Key Decisions
|
|
143
|
+
|
|
144
|
+
#### Decision 1: Tokenization stays on `vtexpayments.com.br`, untouched
|
|
145
|
+
|
|
146
|
+
- **Status**: Accepted
|
|
147
|
+
- **Context**: PCI compliance requires card data tokenization to happen in a scope that already meets those requirements; the Jira ticket is explicit that only "the BFF call onward" moves.
|
|
148
|
+
- **Decision**: `TokenClient` and its call to `vtexpayments.com.br` are not modified by this work.
|
|
149
|
+
- **Consequences**: `saved-cards` never receives raw card data — only the token, `paymentSystem`, and `recaptchaV3Token` it already expects.
|
|
150
|
+
|
|
151
|
+
#### Decision 2: Migrate only the four operations `saved-cards` already implements
|
|
152
|
+
|
|
153
|
+
- **Status**: Accepted
|
|
154
|
+
- **Context**: `saved-cards`'s real, live contract (TypeSpec + controllers + tests) exposes exactly four operations: payment session, add card, list cards, delete card. Validators and default-value management are not implemented server-side.
|
|
155
|
+
- **Decision**: Migrate session/register/list/delete; leave validators and default-value toggling on the BFF.
|
|
156
|
+
- **Consequences**: The credit-card feature area temporarily calls two backends (BFF for validators/defaults, `saved-cards` for the rest) until those remaining endpoints exist server-side — at which point a follow-up migration can finish the cutover.
|
|
157
|
+
|
|
158
|
+
#### Decision 3: Add a dedicated `Vtex_session` header, scoped to the new client only
|
|
159
|
+
|
|
160
|
+
- **Status**: Accepted
|
|
161
|
+
- **Context**: `saved-cards`'s register endpoint reads `Vtex_session` as its own HTTP header and 400s without it; the plugin's shared cookie utility currently strips `vtex_session` from every outbound call.
|
|
162
|
+
- **Decision**: The new client reads the raw `vtex_session` cookie value and sends it as a `Vtex_session` header on its own requests, without altering the shared utility's behavior for other (BFF) calls.
|
|
163
|
+
- **Consequences**: One extra, explicit piece of session plumbing lives in the new client; no behavior change for any existing BFF call.
|
|
164
|
+
|
|
165
|
+
#### Decision 4: `Authorization: Bearer` is not required — the cookie alone authenticates
|
|
166
|
+
|
|
167
|
+
- **Status**: Accepted
|
|
168
|
+
- **Context**: `saved-cards`'s own integration tests always send `Authorization: Bearer <token>` alongside `VtexIdclientAutCookie`, which left it ambiguous whether Bearer was mandatory in the real (non-test-double) auth path. Resolved empirically against the live production service (`https://b2bfaststoredev.vtexcommercestable.com.br/api/saved-cards/credit-card`, `x-vtex-janus-router-backend-app: saved-cards-v0.0.7-prd-005`), using a real authenticated VTEX ID session, on the read-only `list` endpoint (no side effects):
|
|
169
|
+
- Cookie only, no `Authorization` header → `500` (past authentication; failed downstream because the test identity — an admin/CLI session — carries no buyer `customerId` claim, which a real storefront buyer's cookie does carry).
|
|
170
|
+
- `Authorization: Bearer` only, no cookie → `401 Unauthorized`.
|
|
171
|
+
- Cookie + `Authorization: Bearer` together → `500`, identical to cookie-only.
|
|
172
|
+
- Adding the Bearer header changed nothing; omitting the cookie broke auth. This conclusively shows the `VtexIdclientAutCookie` cookie is the credential that matters, and `Authorization: Bearer` is not required in addition to it.
|
|
173
|
+
- **Decision**: The plugin does not need to add an `Authorization: Bearer` header. Continue forwarding `VtexIdclientAutCookie` exactly as it does today.
|
|
174
|
+
- **Consequences**: No new auth-header plumbing is needed beyond Decision 3's `Vtex_session` header. Removes what was the single largest implementation risk in this migration.
|
|
175
|
+
- **Scope of this evidence**: this isolates the auth *layer* only (via `401` vs. non-`401`, which doesn't require a `200` to be conclusive). See Decision 6 for end-to-end confirmation with a real buyer session.
|
|
176
|
+
|
|
177
|
+
#### Decision 6 (superseded by Decision 7 — see below): initial claim that `list` and `delete` were verified end-to-end
|
|
178
|
+
|
|
179
|
+
- **Status**: Superseded — partially correct, partially wrong. Kept here (rather than deleted) so the record shows how the mistake was found and corrected.
|
|
180
|
+
- **What was originally claimed**: linking this branch into a local `faststore dev` server and exercising `list`/`delete` in-browser with a real buyer session showed `list` rendering 10 real cards, and `delete` producing the app's existing success toast on two different cards — taken as end-to-end confirmation of both.
|
|
181
|
+
- **What was actually true (found afterward, see Decision 7)**: `list`'s success was real — it runs via the page's server-side `loader` function (Node `fetch`, not subject to browser CORS). But `delete` runs client-side via `useMutation`, invoked directly from the drawer's `onClick` handler — a genuine cross-origin browser `fetch()` to `saved-cards`. `useMutation` (`src/features/shared/hooks/useMutation.ts`) catches any error from `mutationFn` internally and never rethrows it (`catch (err) { setError(error); options?.onError?.(error); } ... return null;`); `DeleteCreditCardDrawer.handleConfirmClick` calls `await deleteCreditCard(...)` and then unconditionally shows the success toast and reloads, without checking the mutation's error state. So the "success" toast proves nothing about whether the request actually reached the server — see Decision 7 for what was actually happening underneath.
|
|
182
|
+
- **Consequences**: This is a caution for future verification work in this codebase: a passing UI success state (toast, no visible error) is not reliable evidence a `useMutation`-based call succeeded, because errors are swallowed silently unless the caller explicitly checks the returned `error`/`data`. Prefer instrumenting the actual network call (or checking server response state directly) over trusting UI feedback for any operation wired this way.
|
|
183
|
+
|
|
184
|
+
#### Decision 7: `saved-cards` sends no CORS headers — every client-side (browser) call to it is broken, in every environment
|
|
185
|
+
|
|
186
|
+
- **Status**: Accepted (as a finding — the fix itself is not yet decided, see Risks & Mitigations)
|
|
187
|
+
- **Context**: Attempting the `register` flow live (Decision 6's follow-up) surfaced a Next.js runtime error: `ClientError: Failed to fetch` from `Client.ts`, thrown from inside `CreateCreditCardDrawer.handleConfirmClick`, which calls `getPaymentSessionService` directly from a browser click handler. Reproduced directly via `fetch()` executed in the browser's own JS context against `https://b2bfaststoredev.vtexcommercestable.com.br/api/saved-cards/payments/session` (and separately against the `credit-card/{tokenId}` DELETE route) — both throw `TypeError: Failed to fetch` from the browser, with and without `credentials: "include"`. Confirmed via `curl` from the same machine (which is not subject to browser CORS) that this is specifically a CORS gap, not a connectivity problem: an `OPTIONS` preflight to the same route returns `405 Method Not Allowed` (`allow: GET` only — no CORS preflight handling at all), and a plain `GET` sent with an `Origin: http://localhost:3006` header gets back **zero** `Access-Control-Allow-*` headers of any kind.
|
|
188
|
+
- **Why this isn't a `localhost`-specific artifact**: CORS is enforced by the browser based on the *response's* headers, not anything about the calling origin being "known" or "unknown" ahead of time. Since the server returns no `Access-Control-Allow-Origin` header for *any* origin (confirmed by explicitly sending one and getting nothing back), this would block a browser call from the real production storefront domain exactly the same way it blocks `localhost:3006`. This is not something a follow-up "just allow-list the real domain" fix on the client side can work around — the server has to opt in.
|
|
189
|
+
- **What this actually breaks**: any call to `SavedCardsClient` triggered from a browser event handler (not a server-rendered `loader`/`getServerSideProps`-style function) fails outright with a swallowed or visible network error, in every environment, not just local dev. Concretely, as implemented in this PR:
|
|
190
|
+
- `register` (`CreateCreditCardDrawer.handleConfirmClick` → `getPaymentSessionService`/`saveCreditCardService`) — **broken**, confirmed via a real (aborted) attempt in-browser.
|
|
191
|
+
- `delete` (`DeleteCreditCardDrawer.handleConfirmClick` → `useDeleteCreditCard`/`useMutation` → `deleteCreditCardService`) — **broken**, confirmed via a direct `fetch()` reproduction; previously miscredited as working (see Decision 6).
|
|
192
|
+
- `list`'s *initial* page load is unaffected (server-side `loader`), but `useGetCreditCards`/`useQuery`-based client-side refetches of the list (if triggered anywhere client-side) would hit the same wall.
|
|
193
|
+
- **Decision**: Escalated to the `saved-cards` team (`team-b2b`) to add proper CORS support (`Access-Control-Allow-Origin` matching real storefront domains, plus an `OPTIONS` preflight handler). This is a backend-owned fix — the frontend code in this PR is correct as written against the documented contract; it's the service that's missing a required capability for browser-direct callers. The same-origin-proxy alternative (routing calls through a Next.js API route in this plugin) was considered and explicitly rejected for now: it would work around the gap rather than fix it, and would duplicate auth-header plumbing (Decisions 3/4) into a new proxy layer for what is a backend defect, not a frontend design flaw.
|
|
194
|
+
- **Consequences**: This PR (`feat/credit-card-saved-cards-migration`) is intentionally left unchanged — `register`/`delete` will start working once `saved-cards` ships CORS support, with no frontend code changes required. Re-verify both operations live once that lands (do not trust unit tests or UI toasts alone — see Decision 6).
|
|
195
|
+
|
|
196
|
+
#### Decision 5: Production base host is `https://{account}.vtexcommercestable.com.br/api/saved-cards`
|
|
197
|
+
|
|
198
|
+
- **Status**: Accepted
|
|
199
|
+
- **Context**: The only host information in the team's Postman collection was `localhost:5001` (local dev). Resolved empirically: `GET https://qastore.vtexcommercestable.com.br/api/saved-cards/credit-card` returns a clean `401` with `x-vtex-janus-router-backend-app: saved-cards-v0.0.7-prd-005` and an RFC 9110 `ProblemDetails` body matching the service's own `AddProblemDetails()` wiring — i.e., the request reaches the real, live `saved-cards` production app through the standard account-scoped commerce host. (`.../meta/healthcheck` on the same host hits an unrelated legacy VTEX Portal redirect quirk and returns a misleading `400` from a different, older subsystem — not evidence against this host; the `credit-card` route is the reliable probe.)
|
|
200
|
+
- **Decision**: Use `https://{account}.vtexcommercestable.com.br/api/saved-cards` as the base URL, with `{account}` resolved the same way the plugin already resolves `storeConfig.api.storeId` for its other clients.
|
|
201
|
+
- **Consequences**: No infra/team confirmation needed before implementation; the base URL can be added to `src/features/shared/utils/api.ts` following the existing `getApiUrl`/`getTokenizationUrl` pattern.
|
|
202
|
+
|
|
203
|
+
### Implementation Plan
|
|
204
|
+
|
|
205
|
+
1. **Add `SavedCardsClient`** (session/add/list/delete), forwarding `VtexIdclientAutCookie` (as today) plus the new `Vtex_session` header (Decision 3); no `Authorization: Bearer` needed (Decision 4); base URL per Decision 5.
|
|
206
|
+
3. **Repoint the four service functions** (`getPaymentSessionService`, `saveCreditCardService`, `getCreditCardsListService`, `deleteCreditCardService`) to the new client; leave `getCreditCardValidatorsService` and default-value services untouched.
|
|
207
|
+
4. **Treat `cardLabel` as optional** in `CreditCardData` (the existing fallback covers cards saved without a nickname; verify no code path assumes the field is always present).
|
|
208
|
+
5. **Resolve Decision 7 (CORS) before anything else ships** — `register` and `delete` are confirmed non-functional as implemented. Choose and implement either a `saved-cards`-side CORS fix or a same-origin proxy route in this plugin.
|
|
209
|
+
6. **Re-validate `register` and `delete` live** with a real buyer session after Decision 7 lands — do not trust unit tests or UI toasts alone (see Decision 6); confirm the actual network call reaches `saved-cards` and returns a real response.
|
|
210
|
+
7. Also confirm behavior for a buyer without `UseAdHocCard` and a forced `saved-cards` 502.
|
|
211
|
+
6. **Add a `CHANGELOG.md` entry** under `[Unreleased]` describing the backend swap (merchant-visible only insofar as error behavior or the missing card label could surface — otherwise mark as internal-only per repo convention).
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 3. Technical Contract
|
|
216
|
+
|
|
217
|
+
### Data Models
|
|
218
|
+
|
|
219
|
+
From the `saved-cards` TypeSpec contract (`.vtex/definitions/models/*.tsp`):
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
PaymentSession {
|
|
223
|
+
id: string
|
|
224
|
+
name: string
|
|
225
|
+
expiresAt: utcDateTime
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
PaymentTokenRequest {
|
|
229
|
+
creditCardToken: string
|
|
230
|
+
paymentSystem: string
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
AddCardRequest {
|
|
234
|
+
token: PaymentTokenRequest
|
|
235
|
+
recaptchaV3Token: string
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
PaymentFieldResponse {
|
|
239
|
+
name: string
|
|
240
|
+
value: string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
AddCardResponse {
|
|
244
|
+
status: string
|
|
245
|
+
paymentSystemId?: string
|
|
246
|
+
accountId?: string
|
|
247
|
+
transactionId?: string
|
|
248
|
+
paymentField?: PaymentFieldResponse[]
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
CreditCard {
|
|
252
|
+
accountId: string (uuid)
|
|
253
|
+
bin: string (max 6 chars)
|
|
254
|
+
cardNumber: string
|
|
255
|
+
cardLabel?: string
|
|
256
|
+
paymentSystem: string
|
|
257
|
+
paymentSystemName: string
|
|
258
|
+
isDefault: boolean
|
|
259
|
+
isActive: boolean
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Note: `cardLabel` is optional — `saved-cards` omits it for cards saved without a nickname. See [Non-Functional Requirements](#non-functional-requirements).
|
|
264
|
+
|
|
265
|
+
### Interfaces
|
|
266
|
+
|
|
267
|
+
All four operations are rooted at `/api/saved-cards` and require the auth described in [Integration Points](#integration-points).
|
|
268
|
+
|
|
269
|
+
| Operation | Method & Path | Query | Request Body | Success Response | Typed Errors |
|
|
270
|
+
|---|---|---|---|---|---|
|
|
271
|
+
| Get payment session | `GET /payments/session` | `?an={account}` | — | `200 PaymentSession` | `502 DownstreamError` |
|
|
272
|
+
| Add credit card | `POST /credit-card` | `?an={account}` | `AddCardRequest` | `200 AddCardResponse` | `400 PaymentTokenRequiredError`, `400 SessionRequiredError`, `422 TransactionDeniedError`, `500 CardSaveFailedError`, `502 DownstreamError` |
|
|
273
|
+
| List credit cards | `GET /credit-card` | `?an={account}` | — | `200 CreditCard[]` | `502 DownstreamError` |
|
|
274
|
+
| Delete credit card | `DELETE /credit-card/{tokenId}` | `?an={account}` | — | `204 No Content` | `404 CardNotFoundError`, `502 DownstreamError` |
|
|
275
|
+
|
|
276
|
+
All four operations also return `401 Unauthorized` when authentication is missing/invalid (verified directly against the service's integration tests).
|
|
277
|
+
|
|
278
|
+
### Integration Points
|
|
279
|
+
|
|
280
|
+
- **Base URL**: `https://{account}.vtexcommercestable.com.br/api/saved-cards` — confirmed empirically against the live production service (Decision 5). The Postman collection's `http://localhost:5001` remains valid for local dev only.
|
|
281
|
+
- **Account resolution**: `?an={account}` query parameter, using the same `storeConfig.api.storeId` value the plugin already resolves today (`getApiUrl`/`getTokenizationUrl` pattern in `src/features/shared/utils/api.ts`).
|
|
282
|
+
- **Identity resolution**: `customerId` and `unitId` are **no longer passed explicitly** by the client — `saved-cards` derives them from the authenticated identity (decoded from the auth cookie/token) via its own `RequestContextResolver`. The client only needs to ensure the auth headers below are present and correct; it does not need to know or forward `customerId`/`unitId` for these four calls.
|
|
283
|
+
- **Required headers**:
|
|
284
|
+
- `Cookie: VtexIdclientAutCookie=<token>` — already sent by the plugin today via `getAuthFromCookie`; confirmed (Decision 4) to be sufficient on its own for authentication.
|
|
285
|
+
- `Vtex_session: <token>` — **new**, sourced from the raw `vtex_session` browser cookie; required specifically by the add-card operation (`400 SESSION_REQUIRED` if missing, confirmed from source), sent on all four calls for consistency.
|
|
286
|
+
- `Authorization: Bearer` — **not required** (Decision 4); no change needed here.
|
|
287
|
+
- **Downstream trust boundary**: `saved-cards` never forwards the buyer's inbound `Authorization`/`VtexIdclientAutCookie` to its own downstream calls (Checkout, Gateway, Profile, MasterData) — it uses its own service credentials. This is internal to `saved-cards` and requires no action from the plugin.
|
|
288
|
+
- **Tokenization boundary (unchanged)**: `TokenClient` continues to call `https://{account}.vtexpayments.com.br/api/pub/sessions/{paymentSession}/tokens` directly — this is not part of the `saved-cards` integration.
|
|
289
|
+
- **BFF boundary (unchanged)**: validators (`GET .../credit-cards/validators`) and default-value management (`POST`/`DELETE .../default-values`) continue to call `buyer-portal-graphql` via the existing `CreditCardClient`/`DefaultValuesClient`.
|
|
290
|
+
|
|
291
|
+
### Invariants & Constraints
|
|
292
|
+
|
|
293
|
+
- No card PAN, CVV, or expiration date is ever included in any request to `saved-cards` — only the opaque `creditCardToken` produced by the existing tokenization step.
|
|
294
|
+
- The `Vtex_session` header value is never logged, and the shared `getCookieWithoutSessionObjects` utility's existing stripping behavior for other (BFF) calls is not altered by this change.
|
|
295
|
+
- This migration touches exactly the four operations listed in [Interfaces](#interfaces); validators and default-value management are explicitly out of scope and must keep functioning against the BFF unchanged.
|
|
296
|
+
- The identifier passed as `tokenId` on delete is the same `accountId` value the UI already uses today (from the list response) — no new identifier concept is introduced.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Client } from "../../shared/clients/Client";
|
|
2
|
+
import { getAccountName, getSavedCardsUrl } from "../../shared/utils";
|
|
3
|
+
import { getVtexSessionFromCookie } from "../utils";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AddCardRequest,
|
|
7
|
+
AddCardResponse,
|
|
8
|
+
CreditCardData,
|
|
9
|
+
PaymentSession,
|
|
10
|
+
} from "../types";
|
|
11
|
+
|
|
12
|
+
type SavedCardsContext = {
|
|
13
|
+
cookie: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Client for the `saved-cards` microservice (see
|
|
18
|
+
* specs/credit-card-saved-cards-migration.md). Covers the four operations
|
|
19
|
+
* `saved-cards` implements today: payment session, add card, list cards,
|
|
20
|
+
* delete card. Validators and default-value toggling stay on the BFF via
|
|
21
|
+
* `CreditCardClient`/`DefaultValuesClient` — this client does not cover them.
|
|
22
|
+
*
|
|
23
|
+
* Auth: forwards `VtexIdclientAutCookie` as today, plus a dedicated
|
|
24
|
+
* `Vtex_session` header sourced from the raw `vtex_session` cookie value
|
|
25
|
+
* (Decision 3) — required by the add-card endpoint. No `Authorization:
|
|
26
|
+
* Bearer` header is sent (Decision 4, verified empirically against the live
|
|
27
|
+
* service).
|
|
28
|
+
*/
|
|
29
|
+
export default class SavedCardsClient extends Client {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(getSavedCardsUrl());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private buildHeaders(cookie: string) {
|
|
35
|
+
return {
|
|
36
|
+
Cookie: cookie,
|
|
37
|
+
Vtex_session: getVtexSessionFromCookie(cookie),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private buildParams() {
|
|
42
|
+
return { an: getAccountName() };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getPaymentSession({ cookie }: SavedCardsContext) {
|
|
46
|
+
return this.get<PaymentSession>("/payments/session", {
|
|
47
|
+
headers: this.buildHeaders(cookie),
|
|
48
|
+
params: this.buildParams(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addCreditCard(data: AddCardRequest, { cookie }: SavedCardsContext) {
|
|
53
|
+
return this.post<AddCardResponse, AddCardRequest>("/credit-card", data, {
|
|
54
|
+
headers: this.buildHeaders(cookie),
|
|
55
|
+
params: this.buildParams(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
listCreditCards({ cookie }: SavedCardsContext) {
|
|
60
|
+
return this.get<CreditCardData[]>("/credit-card", {
|
|
61
|
+
headers: this.buildHeaders(cookie),
|
|
62
|
+
params: this.buildParams(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
deleteCreditCard(tokenId: string, { cookie }: SavedCardsContext) {
|
|
67
|
+
return this.request<void, never>({
|
|
68
|
+
url: `/credit-card/${tokenId}`,
|
|
69
|
+
method: "DELETE",
|
|
70
|
+
headers: this.buildHeaders(cookie),
|
|
71
|
+
params: this.buildParams(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const savedCardsClient = new SavedCardsClient();
|
|
77
|
+
|
|
78
|
+
export { savedCardsClient };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("discovery.config", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
api: { storeId: "test-store" },
|
|
6
|
+
secureSubdomain: "https://test.vtex.com",
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// The shared/utils barrel re-exports React components — mock it to avoid
|
|
11
|
+
// loading React in the Node test environment.
|
|
12
|
+
vi.mock("../../../shared/utils", () => ({
|
|
13
|
+
getSavedCardsUrl: () =>
|
|
14
|
+
"https://test-store.vtexcommercestable.com.br/api/saved-cards",
|
|
15
|
+
getAccountName: () => "test-store",
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { savedCardsClient } from "../SavedCardsClient";
|
|
19
|
+
|
|
20
|
+
const mockCookie =
|
|
21
|
+
"VtexIdclientAutCookie_test-store=auth-token;vtex_session=session-token-xyz";
|
|
22
|
+
|
|
23
|
+
function makeFetchMock(body: unknown, status = 200) {
|
|
24
|
+
return vi.fn().mockResolvedValue({
|
|
25
|
+
ok: status >= 200 && status < 300,
|
|
26
|
+
status,
|
|
27
|
+
statusText: status >= 200 && status < 300 ? "OK" : "Error",
|
|
28
|
+
headers: { get: () => "application/json" },
|
|
29
|
+
json: () => Promise.resolve(body),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("SavedCardsClient", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.stubGlobal("fetch", makeFetchMock({}));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.unstubAllGlobals();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("getPaymentSession", () => {
|
|
43
|
+
it("calls GET /payments/session with ?an={account} against the saved-cards host", async () => {
|
|
44
|
+
const mockSession = {
|
|
45
|
+
id: "session-1",
|
|
46
|
+
name: "sess",
|
|
47
|
+
expiresAt: "2026-07-27T00:00:00Z",
|
|
48
|
+
};
|
|
49
|
+
vi.stubGlobal("fetch", makeFetchMock(mockSession));
|
|
50
|
+
|
|
51
|
+
const result = await savedCardsClient.getPaymentSession({
|
|
52
|
+
cookie: mockCookie,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const [url] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
56
|
+
expect(url).toBe(
|
|
57
|
+
"https://test-store.vtexcommercestable.com.br/api/saved-cards/payments/session?an=test-store"
|
|
58
|
+
);
|
|
59
|
+
expect(result).toEqual(mockSession);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("sends the VtexIdclientAutCookie cookie and a Vtex_session header sourced from the raw vtex_session cookie", async () => {
|
|
63
|
+
vi.stubGlobal("fetch", makeFetchMock({}));
|
|
64
|
+
|
|
65
|
+
await savedCardsClient.getPaymentSession({ cookie: mockCookie });
|
|
66
|
+
|
|
67
|
+
const [, config] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
68
|
+
expect(config.headers?.Vtex_session).toBe("session-token-xyz");
|
|
69
|
+
expect(config.headers?.Cookie).toBe(mockCookie);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not send an Authorization header", async () => {
|
|
73
|
+
vi.stubGlobal("fetch", makeFetchMock({}));
|
|
74
|
+
|
|
75
|
+
await savedCardsClient.getPaymentSession({ cookie: mockCookie });
|
|
76
|
+
|
|
77
|
+
const [, config] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
78
|
+
expect(config.headers?.Authorization).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("addCreditCard", () => {
|
|
83
|
+
it("POSTs to /credit-card with ?an={account} and the AddCardRequest body", async () => {
|
|
84
|
+
const mockResponse = { status: "APPROVED", accountId: "card-1" };
|
|
85
|
+
vi.stubGlobal("fetch", makeFetchMock(mockResponse));
|
|
86
|
+
|
|
87
|
+
const requestBody = {
|
|
88
|
+
token: { creditCardToken: "tok-123", paymentSystem: "visa" },
|
|
89
|
+
recaptchaV3Token: "recaptcha-abc",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = await savedCardsClient.addCreditCard(requestBody, {
|
|
93
|
+
cookie: mockCookie,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const [url, config] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
97
|
+
expect(url).toBe(
|
|
98
|
+
"https://test-store.vtexcommercestable.com.br/api/saved-cards/credit-card?an=test-store"
|
|
99
|
+
);
|
|
100
|
+
expect(config.method).toBe("POST");
|
|
101
|
+
expect(JSON.parse(config.body as string)).toEqual(requestBody);
|
|
102
|
+
expect(result).toEqual(mockResponse);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("sends the Vtex_session header on register too", async () => {
|
|
106
|
+
vi.stubGlobal("fetch", makeFetchMock({}));
|
|
107
|
+
|
|
108
|
+
await savedCardsClient.addCreditCard(
|
|
109
|
+
{
|
|
110
|
+
token: { creditCardToken: "tok", paymentSystem: "visa" },
|
|
111
|
+
recaptchaV3Token: "r",
|
|
112
|
+
},
|
|
113
|
+
{ cookie: mockCookie }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const [, config] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
117
|
+
expect(config.headers?.Vtex_session).toBe("session-token-xyz");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/features/credit-cards/components/CreditCardDropdownMenu/CreditCardDropdownMenu.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { useDrawerProps } from "../../../shared/hooks";
|
|
|
8
8
|
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
9
9
|
import { useSetDefaultCreditCard } from "../../hooks/useSetDefaultsCreditCard";
|
|
10
10
|
import { useUnsetDefaultCreditCard } from "../../hooks/useUnsetDefaultsCreditCard";
|
|
11
|
+
import { getCreditCardDisplayName } from "../../utils";
|
|
11
12
|
|
|
12
13
|
import type { CreditCardData } from "../../types";
|
|
13
14
|
|
|
@@ -158,7 +159,7 @@ export const CreditCardDropdownMenu = ({
|
|
|
158
159
|
{isOpenDeleteDrawer && (
|
|
159
160
|
<DeleteCreditCardDrawer
|
|
160
161
|
readonly
|
|
161
|
-
cardName={currentCreditCard
|
|
162
|
+
cardName={getCreditCardDisplayName(currentCreditCard)}
|
|
162
163
|
cardId={currentCreditCard.accountId}
|
|
163
164
|
isOpen={isOpenDeleteDrawer}
|
|
164
165
|
{...deleteDrawerProps}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { CreditCardSettingsDrawer } from "../../components/CreditCardSettingsDrawer/CreditCardSettingsDrawer";
|
|
24
24
|
import { useChangeCardScope } from "../../hooks";
|
|
25
25
|
import { useGetCreditCards } from "../../hooks/useGetCreditCards";
|
|
26
|
+
import { getCreditCardDisplayName } from "../../utils";
|
|
26
27
|
|
|
27
28
|
import type { CreditCardData } from "../../types";
|
|
28
29
|
|
|
@@ -164,7 +165,7 @@ export const CreditCardLayout = ({ data }: CreditCardsLayoutProps) => {
|
|
|
164
165
|
<Table.Row
|
|
165
166
|
iconName="CreditCard"
|
|
166
167
|
iconSize={24}
|
|
167
|
-
title={creditCard
|
|
168
|
+
title={getCreditCardDisplayName(creditCard)}
|
|
168
169
|
actionIcons={renderRowActions(creditCard)}
|
|
169
170
|
dropdownMenu={
|
|
170
171
|
<CreditCardDropdownMenu
|