@decocms/apps 0.25.1 → 0.26.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.
- package/commerce/sdk/formatPrice.ts +8 -2
- package/package.json +7 -2
- package/resend/actions/send.ts +57 -0
- package/resend/client.ts +30 -0
- package/resend/index.ts +10 -0
- package/resend/types.ts +54 -0
- package/vtex/inline-loaders/productListingPage.ts +18 -0
- package/vtex/utils/transform.ts +8 -7
|
@@ -16,5 +16,11 @@ const formatter = (currency: string, locale: string) => {
|
|
|
16
16
|
return formatters.get(key)!;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export const formatPrice = (
|
|
20
|
-
price
|
|
19
|
+
export const formatPrice = (
|
|
20
|
+
price: number | undefined | null,
|
|
21
|
+
currency = "BRL",
|
|
22
|
+
locale = "pt-BR",
|
|
23
|
+
) =>
|
|
24
|
+
price != null && Number.isFinite(price)
|
|
25
|
+
? formatter(currency, locale).format(price)
|
|
26
|
+
: null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
|
|
6
6
|
"exports": {
|
|
@@ -37,7 +37,11 @@
|
|
|
37
37
|
"./vtex/hooks/*": "./vtex/hooks/*.ts",
|
|
38
38
|
"./vtex/inline-loaders/workflowProducts": "./vtex/inline-loaders/workflowProducts.ts",
|
|
39
39
|
"./vtex/middleware": "./vtex/middleware.ts",
|
|
40
|
-
"./vtex/invoke": "./vtex/invoke.ts"
|
|
40
|
+
"./vtex/invoke": "./vtex/invoke.ts",
|
|
41
|
+
"./resend": "./resend/index.ts",
|
|
42
|
+
"./resend/client": "./resend/client.ts",
|
|
43
|
+
"./resend/types": "./resend/types.ts",
|
|
44
|
+
"./resend/actions/send": "./resend/actions/send.ts"
|
|
41
45
|
},
|
|
42
46
|
"scripts": {
|
|
43
47
|
"typecheck": "tsc --noEmit",
|
|
@@ -68,6 +72,7 @@
|
|
|
68
72
|
"commerce/",
|
|
69
73
|
"shopify/",
|
|
70
74
|
"vtex/",
|
|
75
|
+
"resend/",
|
|
71
76
|
"!**/__tests__/"
|
|
72
77
|
],
|
|
73
78
|
"engines": {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getResendConfig } from "../client";
|
|
2
|
+
import type { CreateEmailOptions, CreateEmailResponse } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Send an email via Resend API.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { sendEmail } from "@decocms/apps/resend/actions/send";
|
|
9
|
+
*
|
|
10
|
+
* const result = await sendEmail({
|
|
11
|
+
* subject: "Hello",
|
|
12
|
+
* html: "<p>World</p>",
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Fields not provided fall back to the defaults set in `configureResend()`.
|
|
17
|
+
*/
|
|
18
|
+
export async function sendEmail(
|
|
19
|
+
payload: Partial<CreateEmailOptions> & { subject?: string; html?: string },
|
|
20
|
+
): Promise<CreateEmailResponse> {
|
|
21
|
+
const config = getResendConfig();
|
|
22
|
+
|
|
23
|
+
const body: CreateEmailOptions = {
|
|
24
|
+
from: payload.from ?? config.emailFrom ?? "Contact <onboarding@resend.dev>",
|
|
25
|
+
to: payload.to ?? config.emailTo ?? [],
|
|
26
|
+
subject: payload.subject ?? config.subject ?? "No subject",
|
|
27
|
+
...(payload.bcc && { bcc: payload.bcc }),
|
|
28
|
+
...(payload.cc && { cc: payload.cc }),
|
|
29
|
+
...(payload.reply_to && { reply_to: payload.reply_to }),
|
|
30
|
+
...(payload.html && { html: payload.html }),
|
|
31
|
+
...(payload.text && { text: payload.text }),
|
|
32
|
+
...(payload.headers && { headers: payload.headers }),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
return {
|
|
48
|
+
data: null,
|
|
49
|
+
error: data,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
data,
|
|
55
|
+
error: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/resend/client.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResendConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
let _config: ResendConfig | null = null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configure the Resend client. Call once in your site's setup.ts.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { configureResend } from "@decocms/apps/resend/client";
|
|
10
|
+
*
|
|
11
|
+
* configureResend({
|
|
12
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
13
|
+
* emailFrom: "Contact <hello@example.com>",
|
|
14
|
+
* emailTo: ["team@example.com"],
|
|
15
|
+
* subject: "Contact form submission",
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function configureResend(config: ResendConfig) {
|
|
20
|
+
_config = config;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getResendConfig(): ResendConfig {
|
|
24
|
+
if (!_config) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Resend not configured. Call configureResend() in setup.ts before using Resend actions.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return _config;
|
|
30
|
+
}
|
package/resend/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { sendEmail } from "./actions/send";
|
|
2
|
+
export { configureResend, getResendConfig } from "./client";
|
|
3
|
+
export type {
|
|
4
|
+
CreateEmailOptions,
|
|
5
|
+
CreateEmailResponse,
|
|
6
|
+
CreateEmailResponseSuccess,
|
|
7
|
+
ErrorResponse,
|
|
8
|
+
ResendConfig,
|
|
9
|
+
ResendErrorCodeKey,
|
|
10
|
+
} from "./types";
|
package/resend/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const RESEND_ERROR_CODES_BY_KEY = {
|
|
2
|
+
missing_required_field: 422,
|
|
3
|
+
invalid_access: 422,
|
|
4
|
+
invalid_parameter: 422,
|
|
5
|
+
invalid_region: 422,
|
|
6
|
+
rate_limit_exceeded: 429,
|
|
7
|
+
missing_api_key: 401,
|
|
8
|
+
invalid_api_Key: 403,
|
|
9
|
+
invalid_from_address: 403,
|
|
10
|
+
validation_error: 403,
|
|
11
|
+
not_found: 404,
|
|
12
|
+
method_not_allowed: 405,
|
|
13
|
+
application_error: 500,
|
|
14
|
+
internal_server_error: 500,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type ResendErrorCodeKey = keyof typeof RESEND_ERROR_CODES_BY_KEY;
|
|
18
|
+
|
|
19
|
+
export interface ErrorResponse {
|
|
20
|
+
message: string;
|
|
21
|
+
name: ResendErrorCodeKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CreateEmailResponseSuccess {
|
|
25
|
+
/** The ID of the newly created email. */
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CreateEmailResponse {
|
|
30
|
+
data: CreateEmailResponseSuccess | null;
|
|
31
|
+
error: ErrorResponse | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CreateEmailOptions {
|
|
35
|
+
from?: string;
|
|
36
|
+
to: string | string[];
|
|
37
|
+
subject: string;
|
|
38
|
+
bcc?: string | string[];
|
|
39
|
+
cc?: string | string[];
|
|
40
|
+
reply_to?: string | string[];
|
|
41
|
+
html?: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResendConfig {
|
|
47
|
+
apiKey: string;
|
|
48
|
+
/** Default sender — e.g. "Contact <onboarding@resend.dev>" */
|
|
49
|
+
emailFrom?: string;
|
|
50
|
+
/** Default recipients */
|
|
51
|
+
emailTo?: string[];
|
|
52
|
+
/** Default subject */
|
|
53
|
+
subject?: string;
|
|
54
|
+
}
|
|
@@ -294,6 +294,24 @@ export default async function vtexProductListingPage(props: PLPProps): Promise<a
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// Handle VTEX `map` query param (e.g. /1368?map=productClusterIds).
|
|
298
|
+
// The `map` param tells IS how to interpret each path segment as a facet type.
|
|
299
|
+
// Segments and map values are positionally matched (comma-separated).
|
|
300
|
+
if (facets.length === 0 && pageUrl && __pagePath) {
|
|
301
|
+
const mapParam = pageUrl.searchParams.get("map");
|
|
302
|
+
if (mapParam) {
|
|
303
|
+
const segments = __pagePath.split("/").filter(Boolean);
|
|
304
|
+
const mapValues = mapParam.split(",");
|
|
305
|
+
for (let i = 0; i < Math.min(segments.length, mapValues.length); i++) {
|
|
306
|
+
const key = mapValues[i].trim();
|
|
307
|
+
const value = decodeURIComponent(segments[i]);
|
|
308
|
+
if (key && value) {
|
|
309
|
+
facets.push({ key, value });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
297
315
|
let pageTypes: PageType[] = [];
|
|
298
316
|
|
|
299
317
|
if (
|
package/vtex/utils/transform.ts
CHANGED
|
@@ -577,7 +577,7 @@ const SHELF_PROPERTY_NAMES = new Set([
|
|
|
577
577
|
*
|
|
578
578
|
* Differences from toProduct():
|
|
579
579
|
* - Images: capped at 2 per SKU (front + back)
|
|
580
|
-
* - Offers:
|
|
580
|
+
* - Offers: best seller only (in-stock first, then cheapest), stripped installments (keeps ListPrice, SalePrice, SRP, PIX, best no-interest)
|
|
581
581
|
* - isVariantOf: single in-stock variant at level 0
|
|
582
582
|
* - additionalProperty: filtered to known-used property names
|
|
583
583
|
* - Drops: description, video, isAccessoryOrSparePartFor, alternateName, gtin, releaseDate, model
|
|
@@ -603,12 +603,13 @@ export const toProductShelf = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
|
603
603
|
}));
|
|
604
604
|
const finalImages = mappedImages.length > 0 ? mappedImages : [DEFAULT_IMAGE];
|
|
605
605
|
|
|
606
|
-
// Offers:
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
606
|
+
// Offers: best seller (in-stock first, then cheapest), lean.
|
|
607
|
+
// Must consider ALL sellers so marketplace products where sellers[0]
|
|
608
|
+
// is OOS but another seller has stock still show as available.
|
|
609
|
+
const offerConverter = isLegacyProduct(product) ? toOfferLegacy : toOffer;
|
|
610
|
+
const allOffers = (sku.sellers ?? []).map(offerConverter).sort(bestOfferFirst);
|
|
611
|
+
const bestOffer = allOffers[0];
|
|
612
|
+
const leanOffers = bestOffer ? [buildOfferShelf(bestOffer)] : [];
|
|
612
613
|
|
|
613
614
|
// isVariantOf: single in-stock variant at level 0
|
|
614
615
|
const isVariantOf =
|