@decocms/apps-vtex 7.2.0

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 (109) hide show
  1. package/package.json +67 -0
  2. package/src/README.md +6 -0
  3. package/src/__tests__/client-segment-cookie.test.ts +255 -0
  4. package/src/__tests__/client-set-cookie-forward.test.ts +257 -0
  5. package/src/actions/address.ts +250 -0
  6. package/src/actions/analytics/sendEvent.ts +86 -0
  7. package/src/actions/auth.ts +333 -0
  8. package/src/actions/checkout.ts +522 -0
  9. package/src/actions/index.ts +11 -0
  10. package/src/actions/masterData.ts +168 -0
  11. package/src/actions/misc.ts +188 -0
  12. package/src/actions/newsletter.ts +105 -0
  13. package/src/actions/orders.ts +34 -0
  14. package/src/actions/profile.ts +201 -0
  15. package/src/actions/session.ts +85 -0
  16. package/src/actions/trigger.ts +43 -0
  17. package/src/actions/wishlist.ts +114 -0
  18. package/src/client.ts +667 -0
  19. package/src/commerceLoaders.ts +261 -0
  20. package/src/hooks/__tests__/createUseCart.test.ts +184 -0
  21. package/src/hooks/__tests__/createUseUser.test.ts +48 -0
  22. package/src/hooks/__tests__/createUseWishlist.test.ts +87 -0
  23. package/src/hooks/createUseCart.ts +360 -0
  24. package/src/hooks/createUseUser.ts +153 -0
  25. package/src/hooks/createUseWishlist.ts +242 -0
  26. package/src/hooks/index.ts +19 -0
  27. package/src/hooks/useAutocomplete.ts +83 -0
  28. package/src/hooks/useCart.ts +243 -0
  29. package/src/hooks/useUser.ts +78 -0
  30. package/src/hooks/useWishlist.ts +119 -0
  31. package/src/index.ts +17 -0
  32. package/src/invoke.ts +181 -0
  33. package/src/loaders/ProductDetailsPage.ts +1 -0
  34. package/src/loaders/ProductList.ts +1 -0
  35. package/src/loaders/ProductListingPage.ts +1 -0
  36. package/src/loaders/address.ts +115 -0
  37. package/src/loaders/autocomplete.ts +58 -0
  38. package/src/loaders/brands.ts +44 -0
  39. package/src/loaders/cart.ts +52 -0
  40. package/src/loaders/catalog.ts +163 -0
  41. package/src/loaders/collections.ts +55 -0
  42. package/src/loaders/index.ts +19 -0
  43. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +20 -0
  44. package/src/loaders/intelligentSearch/productDetailsPage.ts +95 -0
  45. package/src/loaders/intelligentSearch/productList.ts +154 -0
  46. package/src/loaders/intelligentSearch/productListingPage.ts +506 -0
  47. package/src/loaders/intelligentSearch/suggestions.ts +46 -0
  48. package/src/loaders/legacy/productDetailsPage.ts +1 -0
  49. package/src/loaders/legacy/productList.ts +1 -0
  50. package/src/loaders/legacy/relatedProductsLoader.ts +81 -0
  51. package/src/loaders/legacy.ts +635 -0
  52. package/src/loaders/logistics.ts +111 -0
  53. package/src/loaders/minicart.ts +78 -0
  54. package/src/loaders/navbar.ts +26 -0
  55. package/src/loaders/orders.ts +92 -0
  56. package/src/loaders/pageType.ts +68 -0
  57. package/src/loaders/payment.ts +97 -0
  58. package/src/loaders/productListFull.ts +159 -0
  59. package/src/loaders/profile.ts +138 -0
  60. package/src/loaders/promotion.ts +30 -0
  61. package/src/loaders/search.ts +124 -0
  62. package/src/loaders/session.ts +83 -0
  63. package/src/loaders/user.ts +87 -0
  64. package/src/loaders/wishlist.ts +89 -0
  65. package/src/loaders/wishlistProducts.ts +69 -0
  66. package/src/loaders/workflow/products.ts +64 -0
  67. package/src/loaders/workflow.ts +316 -0
  68. package/src/logo.png +0 -0
  69. package/src/middleware.ts +240 -0
  70. package/src/mod.ts +152 -0
  71. package/src/registry.ts +9 -0
  72. package/src/types.ts +248 -0
  73. package/src/utils/__tests__/cookieSanitizer.test.ts +162 -0
  74. package/src/utils/__tests__/fetch.test.ts +80 -0
  75. package/src/utils/__tests__/fetchCache.test.ts +112 -0
  76. package/src/utils/__tests__/instrumentedFetch.test.ts +158 -0
  77. package/src/utils/__tests__/intelligentSearch.test.ts +88 -0
  78. package/src/utils/__tests__/minicart.test.ts +184 -0
  79. package/src/utils/__tests__/operationRouter.test.ts +227 -0
  80. package/src/utils/__tests__/resourceRange.test.ts +32 -0
  81. package/src/utils/__tests__/sitemap.test.ts +185 -0
  82. package/src/utils/__tests__/slugify.test.ts +31 -0
  83. package/src/utils/__tests__/transform.test.ts +698 -0
  84. package/src/utils/accountLoaders.ts +203 -0
  85. package/src/utils/authHelpers.ts +85 -0
  86. package/src/utils/batch.ts +18 -0
  87. package/src/utils/cookieSanitizer.ts +173 -0
  88. package/src/utils/cookies.ts +167 -0
  89. package/src/utils/enrichment.ts +560 -0
  90. package/src/utils/fetch.ts +107 -0
  91. package/src/utils/fetchCache.ts +222 -0
  92. package/src/utils/index.ts +19 -0
  93. package/src/utils/instrumentedFetch.ts +78 -0
  94. package/src/utils/intelligentSearch.ts +96 -0
  95. package/src/utils/legacy.ts +139 -0
  96. package/src/utils/minicart.ts +139 -0
  97. package/src/utils/operationRouter.ts +139 -0
  98. package/src/utils/pickAndOmit.ts +25 -0
  99. package/src/utils/proxy.ts +435 -0
  100. package/src/utils/resourceRange.ts +10 -0
  101. package/src/utils/segment.ts +152 -0
  102. package/src/utils/similars.ts +37 -0
  103. package/src/utils/sitemap.ts +282 -0
  104. package/src/utils/slugCache.ts +28 -0
  105. package/src/utils/slugify.ts +13 -0
  106. package/src/utils/transform.ts +1509 -0
  107. package/src/utils/types.ts +1884 -0
  108. package/src/utils/vtexId.ts +138 -0
  109. package/tsconfig.json +7 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * VTEX Address management actions (store-graphql).
3
+ * Ported from deco-cx/apps:
4
+ * - vtex/actions/address/create.ts
5
+ * - vtex/actions/address/delete.ts
6
+ * - vtex/actions/address/update.ts
7
+ * @see https://developers.vtex.com/docs/guides/profile-system
8
+ */
9
+ import { getVtexConfig, vtexFetch } from "../client";
10
+ import { buildAuthCookieHeader } from "../utils/vtexId";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface AddressInput {
17
+ addressName: string;
18
+ addressType?: string;
19
+ city?: string;
20
+ complement?: string;
21
+ country?: string;
22
+ geoCoordinates?: number[];
23
+ neighborhood?: string;
24
+ number?: string;
25
+ postalCode?: string;
26
+ receiverName?: string;
27
+ reference?: string;
28
+ state?: string;
29
+ street?: string;
30
+ }
31
+
32
+ export interface SavedAddress {
33
+ id: string;
34
+ cacheId: string;
35
+ addressId: string;
36
+ userId?: string;
37
+ addressName: string;
38
+ addressType: string | null;
39
+ city: string | null;
40
+ complement: string | null;
41
+ country: string | null;
42
+ geoCoordinates: number[] | null;
43
+ neighborhood: string | null;
44
+ number: string | null;
45
+ postalCode: string | null;
46
+ receiverName: string | null;
47
+ reference: string | null;
48
+ state: string | null;
49
+ street: string | null;
50
+ name?: string;
51
+ }
52
+
53
+ export interface DeleteAddressResult {
54
+ cacheId: string;
55
+ addresses: SavedAddress[];
56
+ }
57
+
58
+ export interface UpdateAddressResult {
59
+ cacheId: string;
60
+ addresses: SavedAddress;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // GraphQL helper (myvtex.com private graphql)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ interface GqlResponse<T> {
68
+ data: T;
69
+ errors?: Array<{ message: string }>;
70
+ }
71
+
72
+ async function gql<T>(
73
+ query: string,
74
+ variables: Record<string, unknown>,
75
+ authCookie: string,
76
+ ): Promise<T> {
77
+ const { account } = getVtexConfig();
78
+ const result = await vtexFetch<GqlResponse<T>>(
79
+ `https://${account}.myvtex.com/_v/private/graphql/v1`,
80
+ {
81
+ method: "POST",
82
+ body: JSON.stringify({ query, variables }),
83
+ headers: { Cookie: buildAuthCookieHeader(authCookie, account) },
84
+ },
85
+ );
86
+ if (result.errors?.length) {
87
+ throw new Error(`GraphQL error: ${result.errors[0].message}`);
88
+ }
89
+ return result.data;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Mutations
94
+ // ---------------------------------------------------------------------------
95
+
96
+ const SAVE_ADDRESS = `mutation SaveAddress($address: AddressInput!) {
97
+ saveAddress(address: $address) @context(provider: "vtex.store-graphql") {
98
+ addressId
99
+ cacheId
100
+ id
101
+ userId
102
+ receiverName
103
+ complement
104
+ neighborhood
105
+ country
106
+ state
107
+ number
108
+ street
109
+ geoCoordinates
110
+ postalCode
111
+ city
112
+ name
113
+ addressName
114
+ addressType
115
+ }
116
+ }`;
117
+
118
+ const DELETE_ADDRESS = `mutation DeleteAddress($addressId: String) {
119
+ deleteAddress(id: $addressId) {
120
+ cacheId
121
+ addresses {
122
+ addressId: id
123
+ addressType
124
+ addressName
125
+ city
126
+ complement
127
+ country
128
+ neighborhood
129
+ number
130
+ postalCode
131
+ geoCoordinates
132
+ receiverName
133
+ reference
134
+ state
135
+ street
136
+ }
137
+ }
138
+ }`;
139
+
140
+ const UPDATE_ADDRESS = `mutation UpdateAddress($addressId: String!, $addressFields: AddressInput) {
141
+ updateAddress(id: $addressId, fields: $addressFields) @context(provider: "vtex.store-graphql") {
142
+ cacheId
143
+ addresses: address {
144
+ addressId: id
145
+ addressType
146
+ addressName
147
+ city
148
+ complement
149
+ country
150
+ neighborhood
151
+ number
152
+ postalCode
153
+ geoCoordinates
154
+ receiverName
155
+ reference
156
+ state
157
+ street
158
+ }
159
+ }
160
+ }`;
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Actions
164
+ // ---------------------------------------------------------------------------
165
+
166
+ /** Create a new user address. Requires the user's VtexIdclientAutCookie token. */
167
+ export async function createAddress(
168
+ input: AddressInput,
169
+ authCookie: string,
170
+ ): Promise<SavedAddress> {
171
+ const { saveAddress } = await gql<{ saveAddress: SavedAddress }>(
172
+ SAVE_ADDRESS,
173
+ { address: input },
174
+ authCookie,
175
+ );
176
+ return saveAddress;
177
+ }
178
+
179
+ /** Delete an address by its ID. Returns remaining addresses. */
180
+ export async function deleteAddress(
181
+ addressId: string,
182
+ authCookie: string,
183
+ ): Promise<DeleteAddressResult> {
184
+ const { deleteAddress: result } = await gql<{ deleteAddress: DeleteAddressResult }>(
185
+ DELETE_ADDRESS,
186
+ { addressId },
187
+ authCookie,
188
+ );
189
+ return result;
190
+ }
191
+
192
+ /** Update an existing address. Returns the updated address. */
193
+ export async function updateAddress(
194
+ addressId: string,
195
+ fields: Partial<AddressInput>,
196
+ authCookie: string,
197
+ ): Promise<UpdateAddressResult> {
198
+ const { updateAddress: result } = await gql<{ updateAddress: UpdateAddressResult }>(
199
+ UPDATE_ADDRESS,
200
+ {
201
+ addressId,
202
+ addressFields: {
203
+ ...fields,
204
+ receiverName: fields.receiverName ?? null,
205
+ complement: fields.complement ?? null,
206
+ },
207
+ },
208
+ authCookie,
209
+ );
210
+ return result;
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Request-aware wrappers (for COMMERCE_LOADERS / invoke proxy)
215
+ // Handle cookie extraction, postalCode sanitization, and field defaults.
216
+ // ---------------------------------------------------------------------------
217
+
218
+ import { ensureUnsuffixedAuthCookie, getVtexCookies } from "../utils/cookies";
219
+
220
+ function sanitizeAddressInput(props: Record<string, any>): Record<string, any> {
221
+ if (props.postalCode) props.postalCode = props.postalCode.replace(/\D/g, "");
222
+ if (!props.addressName) props.addressName = props.receiverName || `Address ${Date.now()}`;
223
+ if (!props.addressType) props.addressType = "residential";
224
+ return props;
225
+ }
226
+
227
+ export async function createAddressFromRequest(
228
+ props: Record<string, any>,
229
+ request: Request,
230
+ ): Promise<SavedAddress> {
231
+ const cookie = ensureUnsuffixedAuthCookie(getVtexCookies(request));
232
+ return createAddress(sanitizeAddressInput(props) as AddressInput, cookie);
233
+ }
234
+
235
+ export async function updateAddressFromRequest(
236
+ props: Record<string, any>,
237
+ request: Request,
238
+ ): Promise<UpdateAddressResult> {
239
+ const cookie = ensureUnsuffixedAuthCookie(getVtexCookies(request));
240
+ const { addressId, ...fields } = props;
241
+ return updateAddress(addressId, fields, cookie);
242
+ }
243
+
244
+ export async function deleteAddressFromRequest(
245
+ props: Record<string, any>,
246
+ request: Request,
247
+ ): Promise<DeleteAddressResult> {
248
+ const cookie = ensureUnsuffixedAuthCookie(getVtexCookies(request));
249
+ return deleteAddress(props.addressId, cookie);
250
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * VTEX Intelligent Search analytics event.
3
+ *
4
+ * Reads the IS session/anonymous cookies from the incoming request and POSTs
5
+ * an event to the IS event-api. The configured account is resolved via
6
+ * {@link getVtexConfig}.
7
+ *
8
+ * @see https://developers.vtex.com/docs/api-reference/intelligent-search-api#post-/event-api/v1/-account-/event
9
+ */
10
+
11
+ import { getVtexConfig, getVtexFetch } from "../../client";
12
+ import { ANONYMOUS_COOKIE, SESSION_COOKIE } from "../../utils/intelligentSearch";
13
+
14
+ export type Props =
15
+ | {
16
+ type: "session.ping";
17
+ url: string;
18
+ }
19
+ | {
20
+ type: "page.cart";
21
+ products: { productId: string; quantity: number }[];
22
+ }
23
+ | {
24
+ type: "page.empty_cart";
25
+ // Empty array would serialize as an invalid JSON schema, so accept anything.
26
+ products: unknown;
27
+ }
28
+ | {
29
+ type: "page.confirmation";
30
+ order: string;
31
+ products: { productId: string; quantity: number; price: number }[];
32
+ }
33
+ | {
34
+ type: "search.click";
35
+ position: number;
36
+ text: string;
37
+ productId: string;
38
+ url: string;
39
+ }
40
+ | {
41
+ type: "search.query";
42
+ url: string;
43
+ text: string;
44
+ misspelled: boolean;
45
+ match: number;
46
+ operator: string;
47
+ locale: string;
48
+ };
49
+
50
+ const readCookie = (cookieHeader: string, name: string): string | undefined => {
51
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]+)`));
53
+ return match?.[1];
54
+ };
55
+
56
+ /**
57
+ * @title Send Analytics Event
58
+ * @description POST a VTEX Intelligent Search analytics event for the current session.
59
+ */
60
+ const action = async (props: Props, req: Request): Promise<null> => {
61
+ const { account } = getVtexConfig();
62
+ const cookieHeader = req.headers.get("cookie") ?? "";
63
+ const session = readCookie(cookieHeader, SESSION_COOKIE);
64
+ const anonymous = readCookie(cookieHeader, ANONYMOUS_COOKIE);
65
+
66
+ if (!session || !anonymous) {
67
+ throw new Error("Missing IS Cookies");
68
+ }
69
+
70
+ const url = `https://sp.vtex.com/event-api/v1/${account}/event`;
71
+ await getVtexFetch()(url, {
72
+ method: "POST",
73
+ headers: { "content-type": "application/json" },
74
+ body: JSON.stringify({
75
+ ...props,
76
+ session,
77
+ anonymous,
78
+ agent: req.headers.get("user-agent") || "deco-sites/apps",
79
+ }),
80
+ operation: "analytics.event",
81
+ });
82
+
83
+ return null;
84
+ };
85
+
86
+ export default action;
@@ -0,0 +1,333 @@
1
+ /**
2
+ * VTEX Authentication Actions
3
+ *
4
+ * Ported from deco-cx/apps vtex/actions/authentication/*.ts
5
+ * Cookie forwarding happens automatically via RequestContext.responseHeaders.
6
+ * @see https://github.com/deco-cx/apps/tree/main/vtex/actions/authentication
7
+ */
8
+
9
+ import { getVtexConfig, vtexFetchWithCookies } from "../client";
10
+ import { VTEX_AUTH_COOKIE } from "../utils/vtexId";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface AuthProvider {
17
+ providerName: string;
18
+ className: string;
19
+ expectedContext: unknown[];
20
+ }
21
+
22
+ export interface StartAuthentication {
23
+ authenticationToken: string | null;
24
+ oauthProviders: AuthProvider[];
25
+ showClassicAuthentication: boolean;
26
+ showAccessKeyAuthentication: boolean;
27
+ showPasskeyAuthentication: boolean;
28
+ authCookie: string | null;
29
+ isAuthenticated: boolean;
30
+ selectedProvider: string | null;
31
+ samlProviders: unknown[];
32
+ }
33
+
34
+ export interface AuthResponse {
35
+ authStatus: string | "WrongCredentials" | "BlockedUser" | "Success";
36
+ promptMFA: boolean;
37
+ clientToken: string | null;
38
+ authCookie: { Name: string; Value: string } | null;
39
+ accountAuthCookie: { Name: string; Value: string } | null;
40
+ expiresIn: number;
41
+ userId: string | null;
42
+ phoneNumber: string | null;
43
+ scope: string | null;
44
+ }
45
+
46
+ export interface RefreshTokenResponse {
47
+ status: string;
48
+ userId: string;
49
+ refreshAfter: string;
50
+ }
51
+
52
+ /**
53
+ * Cookies to set after a successful login.
54
+ * Caller (server function) should use these to set cookies on the response.
55
+ */
56
+ export interface LoginCookies {
57
+ authCookieName: string;
58
+ authCookieValue: string;
59
+ accountAuthCookieName?: string;
60
+ accountAuthCookieValue?: string;
61
+ expiresInSeconds: number;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const FORM_HEADERS = {
69
+ "Content-Type": "application/x-www-form-urlencoded",
70
+ Accept: "application/json",
71
+ };
72
+
73
+ /**
74
+ * Extract login cookies from an AuthResponse.
75
+ * Returns null if auth failed.
76
+ */
77
+ export function extractLoginCookies(response: AuthResponse): LoginCookies | null {
78
+ if (response.authStatus !== "Success" || !response.authCookie) {
79
+ return null;
80
+ }
81
+ return {
82
+ authCookieName: response.authCookie.Name,
83
+ authCookieValue: response.authCookie.Value,
84
+ accountAuthCookieName: response.accountAuthCookie?.Name,
85
+ accountAuthCookieValue: response.accountAuthCookie?.Value,
86
+ expiresInSeconds: response.expiresIn,
87
+ };
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Actions
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export interface StartAuthenticationProps {
95
+ callbackUrl?: string;
96
+ returnUrl?: string;
97
+ locale?: string;
98
+ appStart?: boolean;
99
+ }
100
+
101
+ export async function startAuthentication(
102
+ props?: StartAuthenticationProps,
103
+ ): Promise<StartAuthentication> {
104
+ const config = getVtexConfig();
105
+ const {
106
+ callbackUrl = "/",
107
+ returnUrl = "/",
108
+ locale = config.locale ?? "pt-BR",
109
+ appStart = true,
110
+ } = props ?? {};
111
+
112
+ const params = new URLSearchParams({
113
+ locale,
114
+ scope: config.account,
115
+ appStart: String(appStart),
116
+ callbackUrl,
117
+ returnUrl,
118
+ });
119
+
120
+ return vtexFetchWithCookies<StartAuthentication>(
121
+ `/api/vtexid/pub/authentication/start?${params}`,
122
+ );
123
+ }
124
+
125
+ export interface ClassicSignInProps {
126
+ email: string;
127
+ password: string;
128
+ authenticationToken?: string;
129
+ }
130
+
131
+ /**
132
+ * Classic email + password sign-in.
133
+ * Calls startAuthentication internally if no authenticationToken provided.
134
+ * Set-Cookie headers from both calls are forwarded via RequestContext.responseHeaders.
135
+ */
136
+ export async function classicSignIn(props: ClassicSignInProps): Promise<AuthResponse> {
137
+ const { email, password } = props;
138
+ let token = props.authenticationToken;
139
+ if (!token) {
140
+ const startResult = await startAuthentication();
141
+ token = startResult.authenticationToken ?? undefined;
142
+ if (!token) throw new Error("Failed to obtain authentication token from startAuthentication");
143
+ }
144
+
145
+ const body = new URLSearchParams({
146
+ email,
147
+ password,
148
+ authenticationToken: token,
149
+ });
150
+ return vtexFetchWithCookies<AuthResponse>("/api/vtexid/pub/authentication/classic/validate", {
151
+ method: "POST",
152
+ body,
153
+ headers: FORM_HEADERS,
154
+ });
155
+ }
156
+
157
+ export interface AccessKeySignInProps {
158
+ email: string;
159
+ accessKey: string;
160
+ authenticationToken: string;
161
+ }
162
+
163
+ /**
164
+ * Passwordless sign-in via email access key.
165
+ */
166
+ export async function accessKeySignIn(props: AccessKeySignInProps): Promise<AuthResponse> {
167
+ const { email, accessKey, authenticationToken } = props;
168
+ const body = new URLSearchParams({
169
+ login: email,
170
+ accessKey,
171
+ authenticationToken,
172
+ });
173
+
174
+ return vtexFetchWithCookies<AuthResponse>("/api/vtexid/pub/authentication/accesskey/validate", {
175
+ method: "POST",
176
+ body,
177
+ headers: FORM_HEADERS,
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Logout — returns list of cookie names that must be cleared (Max-Age=0).
183
+ */
184
+ export function logout(): { cookiesToClear: string[] } {
185
+ const { account } = getVtexConfig();
186
+ return {
187
+ cookiesToClear: [
188
+ VTEX_AUTH_COOKIE,
189
+ `${VTEX_AUTH_COOKIE}_${account}`,
190
+ "vid_rt",
191
+ `vid_rt_${account}`,
192
+ ],
193
+ };
194
+ }
195
+
196
+ export interface RefreshTokenProps {
197
+ fingerprint?: string;
198
+ }
199
+
200
+ /**
201
+ * Refreshes the VTEX auth token using existing session cookies.
202
+ * Cookies are read automatically from RequestContext.
203
+ */
204
+ export async function refreshToken(props?: RefreshTokenProps): Promise<RefreshTokenResponse> {
205
+ return vtexFetchWithCookies<RefreshTokenResponse>("/api/vtexid/refreshtoken/webstore", {
206
+ method: "POST",
207
+ body: JSON.stringify({ fingerprint: props?.fingerprint }),
208
+ });
209
+ }
210
+
211
+ export interface RecoveryPasswordProps {
212
+ email: string;
213
+ newPassword: string;
214
+ accessKey: string;
215
+ authenticationToken: string;
216
+ locale?: string;
217
+ }
218
+
219
+ /**
220
+ * Sets a new password using an email access key (password-recovery flow).
221
+ */
222
+ export async function recoveryPassword(props: RecoveryPasswordProps): Promise<AuthResponse> {
223
+ const { email, newPassword, accessKey, authenticationToken, locale } = props;
224
+ const config = getVtexConfig();
225
+
226
+ const params = new URLSearchParams({
227
+ scope: config.account,
228
+ locale: locale ?? config.locale ?? "pt-BR",
229
+ });
230
+
231
+ const body = new URLSearchParams({
232
+ login: email,
233
+ accessKey,
234
+ newPassword,
235
+ authenticationToken,
236
+ });
237
+
238
+ return vtexFetchWithCookies<AuthResponse>(
239
+ `/api/vtexid/pub/authentication/classic/setpassword?${params}`,
240
+ { method: "POST", body, headers: FORM_HEADERS },
241
+ );
242
+ }
243
+
244
+ export interface ResetPasswordProps {
245
+ email: string;
246
+ currentPassword: string;
247
+ newPassword: string;
248
+ authenticationToken?: string;
249
+ locale?: string;
250
+ }
251
+
252
+ /**
253
+ * Resets password for an already-authenticated user.
254
+ * Calls startAuthentication internally if no authenticationToken provided.
255
+ * Set-Cookie headers from both calls are forwarded via RequestContext.responseHeaders.
256
+ */
257
+ export async function resetPassword(props: ResetPasswordProps): Promise<AuthResponse> {
258
+ const { email, currentPassword, newPassword, locale } = props;
259
+ const config = getVtexConfig();
260
+
261
+ let token = props.authenticationToken;
262
+ if (!token) {
263
+ const startResult = await startAuthentication({ locale });
264
+ token = startResult.authenticationToken ?? undefined;
265
+ if (!token) throw new Error("Failed to obtain authentication token from startAuthentication");
266
+ }
267
+
268
+ const params = new URLSearchParams({
269
+ scope: config.account,
270
+ locale: locale ?? config.locale ?? "pt-BR",
271
+ });
272
+
273
+ const body = new URLSearchParams({
274
+ login: email,
275
+ currentPassword,
276
+ newPassword,
277
+ authenticationToken: token,
278
+ });
279
+
280
+ return vtexFetchWithCookies<AuthResponse>(
281
+ `/api/vtexid/pub/authentication/classic/setpassword?${params}`,
282
+ { method: "POST", body, headers: FORM_HEADERS },
283
+ );
284
+ }
285
+
286
+ export interface SendEmailVerificationProps {
287
+ email: string;
288
+ authenticationToken?: string;
289
+ locale?: string;
290
+ parentAppId?: string;
291
+ }
292
+
293
+ /**
294
+ * Sends an access-key verification email.
295
+ * Calls startAuthentication internally if no authenticationToken provided.
296
+ * Returns { success, authenticationToken }.
297
+ */
298
+ export async function sendEmailVerification(props: SendEmailVerificationProps): Promise<{
299
+ success: boolean;
300
+ authenticationToken: string | null;
301
+ }> {
302
+ const { email, locale, parentAppId } = props;
303
+ try {
304
+ let token = props.authenticationToken;
305
+
306
+ if (!token) {
307
+ const startResult = await startAuthentication({ locale });
308
+ token = startResult.authenticationToken ?? undefined;
309
+ if (!token) throw new Error("Failed to obtain authentication token");
310
+ }
311
+
312
+ const body = new URLSearchParams({ authenticationToken: token, email });
313
+ if (locale) body.append("locale", locale);
314
+ if (parentAppId) body.append("parentAppId", parentAppId);
315
+
316
+ const result = await vtexFetchWithCookies<Record<string, string>>(
317
+ "/api/vtexid/pub/authentication/accesskey/send?deliveryMethod=email",
318
+ { method: "POST", body, headers: FORM_HEADERS },
319
+ );
320
+
321
+ if (result?.authStatus === "InvalidToken") {
322
+ throw new Error("Authentication token is invalid");
323
+ }
324
+
325
+ return {
326
+ success: true,
327
+ authenticationToken: token,
328
+ };
329
+ } catch (error) {
330
+ console.error("[sendEmailVerification]", error);
331
+ return { success: false, authenticationToken: null };
332
+ }
333
+ }