Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion vtex/actions/cart/updateAttachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ export interface Props {
expectedOrderFormSections?: string[];
// deno-lint-ignore no-explicit-any
body: any;
/**
* Client-supplied orderFormId fallback for when the checkout.vtex.com cookie
* is absent on the request (e.g. in-app WebViews with partitioned/ephemeral
* cookies). VTEX addresses the orderForm by the URL path param, so the cookie
* is optional for the write.
*/
orderFormId?: string;
}

/**
Expand All @@ -27,7 +34,14 @@ const action = async (
body,
expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS,
} = props;
const { orderFormId } = parseCookie(req.headers);
// VTEX addresses the orderForm by the URL path param, so the checkout.vtex.com
// cookie is optional for the write. In-app WebViews (Instagram iOS) often drop
// that HttpOnly cookie while the client still holds a valid orderFormId, so fall
// back to the client-supplied props.orderFormId. Keep the throw: a genuinely
// missing id must still error (never no-op — the cart queue would otherwise
// overwrite the local cart signal with an empty orderForm).
const { orderFormId: cookieOrderFormId } = parseCookie(req.headers);
const orderFormId = cookieOrderFormId || props.orderFormId;

if (!orderFormId || orderFormId === "") {
throw new Error("Order form ID is required");
Expand Down
50 changes: 34 additions & 16 deletions vtex/loaders/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,41 @@ const loader = async (
hasDifferentMarketingData(cart.marketingData, marketingData)
) {
const expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS;
const result = await vcsDeprecated
["POST /api/checkout/pub/orderForm/:orderFormId/attachments/:attachment"](
{
orderFormId: cart.orderFormId,
attachment: "marketingData",
sc: segment?.payload.channel,
},
{
body: { expectedOrderFormSections, ...marketingData },
headers: {
accept: "application/json",
"content-type": "application/json",
cookie: withOrderFormIdCookie,
try {
const result = await vcsDeprecated
["POST /api/checkout/pub/orderForm/:orderFormId/attachments/:attachment"](
{
orderFormId: cart.orderFormId,
attachment: "marketingData",
sc: segment?.payload.channel,
},
},
);
return forceHttpsOnAssets((await result.json()) as OrderForm);
{
body: { expectedOrderFormSections, ...marketingData },
headers: {
accept: "application/json",
"content-type": "application/json",
cookie: withOrderFormIdCookie,
},
},
);
return forceHttpsOnAssets((await result.json()) as OrderForm);
} catch (error) {
// This marketingData write only ECHOES attribution VTEX already stored on
// the orderForm (it is a no-op on success). VTEX rejects it with 400
// CHK0038 when a matched campaign audience injects a null marketingTag
// (known issue: marketingtags-with-null-value-when-campaign-audience-is-
// matched). A failed attribution side-write must NOT break the whole cart,
// so serve the already-fetched cart instead of letting the error bubble.
// Do NOT sanitize/strip marketingTags to "fix" this: overwriting VTEX's
// marketingData drops the promotion trigger — it hurt sales and was
// reverted (PR #352). The campaign tags stay intact on the server cart.
logger.warn("marketingData attribution re-POST failed", {
orderFormId: cart.orderFormId,
error: error instanceof Error ? error.message : String(error),
reqUrl: req.url,
});
return forceHttpsOnAssets(cart);
}
}
}

Expand Down