diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index ad9895fa80..14fca63a17 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,5 +1,5 @@ ## v5.1.0-dev -- Bump commerce-sdk-isomorphic to 5.1.0-unstable-20260226081656 +- Bump commerce-sdk-isomorphic to 5.1.0 - Add Node 24 support. Drop Node 16 support. [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) - Add Shopper Consents API support [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) diff --git a/packages/commerce-sdk-react/package-lock.json b/packages/commerce-sdk-react/package-lock.json index 867b2177c7..f857193b7b 100644 --- a/packages/commerce-sdk-react/package-lock.json +++ b/packages/commerce-sdk-react/package-lock.json @@ -9,7 +9,7 @@ "version": "5.1.0-dev", "license": "See license in LICENSE", "dependencies": { - "commerce-sdk-isomorphic": "5.1.0-unstable-20260226081656", + "commerce-sdk-isomorphic": "5.1.0", "js-cookie": "^3.0.1", "jwt-decode": "^4.0.0" }, @@ -920,9 +920,9 @@ "license": "MIT" }, "node_modules/commerce-sdk-isomorphic": { - "version": "5.1.0-unstable-20260226081656", - "resolved": "https://registry.npmjs.org/commerce-sdk-isomorphic/-/commerce-sdk-isomorphic-5.1.0-unstable-20260226081656.tgz", - "integrity": "sha512-YwAJBKh61pU7hkSYL7FGm678iyNqAStI2SpPeAkYyPveBZF+FU69oPRKOIT/ycqX0wJYC4Qy44JdfM7ti6fCIg==", + "version": "5.1.0", + "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/commerce-sdk-isomorphic/-/commerce-sdk-isomorphic-5.1.0.tgz", + "integrity": "sha512-i66SgfB6ml75HT8KGdIQE+Qm05uI66oIHeGXXPDh8jeIXZtccYUmegvzu+BBa68y1FrztcjlINCIjvExGurz3A==", "license": "BSD-3-Clause", "dependencies": { "nanoid": "^3.3.8", diff --git a/packages/commerce-sdk-react/package.json b/packages/commerce-sdk-react/package.json index 09aa8a3ac0..4e1b57aa72 100644 --- a/packages/commerce-sdk-react/package.json +++ b/packages/commerce-sdk-react/package.json @@ -40,7 +40,7 @@ "version": "node ./scripts/version.js" }, "dependencies": { - "commerce-sdk-isomorphic": "5.1.0-unstable-20260226081656", + "commerce-sdk-isomorphic": "5.1.0", "js-cookie": "^3.0.1", "jwt-decode": "^4.0.0" }, diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs index 7d174421f5..af6d6da727 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs @@ -158,11 +158,14 @@ module.exports = { } }, // Salesforce Payments configuration - // Set enabled to false to disable Salesforce Payments even if the Commerce Cloud instance supports it. - // Example URLs: sdkUrl: 'https://.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js' - // metadataUrl: 'https://.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json' + // Set enabled to true to enable Salesforce Payments (requires the Salesforce Payments feature toggle to be enabled on the Commerce Cloud instance). + // Set enabled to false to disable Salesforce Payments on the storefront (the Commerce Cloud feature toggle is unaffected). + // sdkUrl and metadataUrl are hosted on your Commerce Cloud instance. Replace with your instance hostname. + // This may be a demandware.net hostname (e.g., myinstance.unified.demandware.net) or a vanity/custom hostname. + // sdkUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js' + // metadataUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json' sfPayments: { - enabled: true, + enabled: false, sdkUrl: '', metadataUrl: '' }, diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/ssr.js.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/ssr.js.hbs index 08ca5e2412..65389f9e58 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/ssr.js.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/ssr.js.hbs @@ -10,7 +10,6 @@ import crypto from 'crypto' import express from 'express' import helmet from 'helmet' -import https from 'https' import {createRemoteJWKSet as joseCreateRemoteJWKSet, jwtVerify, decodeJwt} from 'jose' import path from 'path' import {getRuntime} from '@salesforce/pwa-kit-runtime/ssr/server/express' @@ -359,7 +358,8 @@ const {handler} = runtime.createHandler(options, (app) => { '*.stripe.com', '*.paypal.com', '*.adyen.com', - '*.google.com', + 'pay.google.com', + 'www.gstatic.com', '*.demandware.net' // Used to load a valid payment scripts in test environment ], 'connect-src': [ @@ -375,7 +375,11 @@ const {handler} = runtime.createHandler(options, (app) => { // Payment gateways '*.demandware.net', // Used to load a valid payment scripts in test environment '*.adyen.com', - '*.google.com' + '*.paypal.com', + 'pay.google.com', + 'payments.google.com', + 'google.com', + 'www.google.com' ], 'frame-src': [ // Allow frames from Salesforce site.com (Needed for MIAW) @@ -383,8 +387,9 @@ const {handler} = runtime.createHandler(options, (app) => { // Payment gateways '*.stripe.com', '*.paypal.com', - '*.google.com', - '*.adyen.com' + '*.adyen.com', + 'payments.google.com', + 'pay.google.com' ] } } @@ -450,49 +455,30 @@ const {handler} = runtime.createHandler(options, (app) => { // Helper function to transform relative icon paths to absolute URLs function transformIconPaths(data, ecomServerHost) { const baseUrl = `https://${ecomServerHost}/on/demandware.static/Sites-Site/-/-/internal` - const dataStr = JSON.stringify(data) - // Replace all relative icon paths with absolute URLs - const transformedStr = dataStr.replace(/"src":\s*"\/icons\//g, `"src":"${baseUrl}/icons/`) - return JSON.parse(transformedStr) + const methodTypes = data?.paymentMethodTypes + if (methodTypes) { + for (const method of Object.values(methodTypes)) { + for (const image of method.images ?? []) { + if (image.src?.startsWith('/icons/')) { + image.src = `${baseUrl}${image.src}` + } + } + } + } + return data } - + + // Helper function to fetch payment metadata from the Commerce Cloud instance app.get('/api/payment-metadata', async (req, res) => { try { - // Parse the URL to extract hostname and path - const url = new URL(config.app.sfPayments.metadataUrl) - // Use Node's https module instead of fetch - const data = await new Promise((resolve, reject) => { - const options = { - hostname: url.hostname, - path: url.pathname, - method: 'GET', - rejectUnauthorized: false, // This bypasses SSL verification - headers: { - Accept: 'application/json' - } - } - - const req = https.request(options, (response) => { - let data = '' - response.on('data', (chunk) => { - data += chunk - }) - response.on('end', () => { - try { - resolve(JSON.parse(data)) - } catch (e) { - reject(e) - } - }) - }) - - req.on('error', reject) - req.end() + const response = await fetch(config.app.sfPayments.metadataUrl, { + headers: { Accept: 'application/json' } }) - - // Transform relative icon paths to absolute URLs - const transformedData = transformIconPaths(data, url.hostname) - + if (!response.ok) { + throw new Error(`Metadata request failed with status: ${response.status}`) + } + const data = await response.json() + const transformedData = transformIconPaths(data, new URL(config.app.sfPayments.metadataUrl).hostname) res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Content-Type', 'application/json') res.json(transformedData) @@ -503,6 +489,7 @@ const {handler} = runtime.createHandler(options, (app) => { }) } }) + app.get('*', runtime.render) }) // SSR requires that we export a single handler function called 'get', that diff --git a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs index 6d020e6f5d..65389f9e58 100644 --- a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs +++ b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs @@ -10,7 +10,6 @@ import crypto from 'crypto' import express from 'express' import helmet from 'helmet' -import https from 'https' import {createRemoteJWKSet as joseCreateRemoteJWKSet, jwtVerify, decodeJwt} from 'jose' import path from 'path' import {getRuntime} from '@salesforce/pwa-kit-runtime/ssr/server/express' @@ -359,7 +358,8 @@ const {handler} = runtime.createHandler(options, (app) => { '*.stripe.com', '*.paypal.com', '*.adyen.com', - '*.google.com', + 'pay.google.com', + 'www.gstatic.com', '*.demandware.net' // Used to load a valid payment scripts in test environment ], 'connect-src': [ @@ -375,7 +375,11 @@ const {handler} = runtime.createHandler(options, (app) => { // Payment gateways '*.demandware.net', // Used to load a valid payment scripts in test environment '*.adyen.com', - '*.google.com' + '*.paypal.com', + 'pay.google.com', + 'payments.google.com', + 'google.com', + 'www.google.com' ], 'frame-src': [ // Allow frames from Salesforce site.com (Needed for MIAW) @@ -383,8 +387,9 @@ const {handler} = runtime.createHandler(options, (app) => { // Payment gateways '*.stripe.com', '*.paypal.com', - '*.google.com', - '*.adyen.com' + '*.adyen.com', + 'payments.google.com', + 'pay.google.com' ] } } @@ -450,49 +455,30 @@ const {handler} = runtime.createHandler(options, (app) => { // Helper function to transform relative icon paths to absolute URLs function transformIconPaths(data, ecomServerHost) { const baseUrl = `https://${ecomServerHost}/on/demandware.static/Sites-Site/-/-/internal` - const dataStr = JSON.stringify(data) - // Replace all relative icon paths with absolute URLs - const transformedStr = dataStr.replace(/"src":\s*"\/icons\//g, `"src":"${baseUrl}/icons/`) - return JSON.parse(transformedStr) + const methodTypes = data?.paymentMethodTypes + if (methodTypes) { + for (const method of Object.values(methodTypes)) { + for (const image of method.images ?? []) { + if (image.src?.startsWith('/icons/')) { + image.src = `${baseUrl}${image.src}` + } + } + } + } + return data } - + + // Helper function to fetch payment metadata from the Commerce Cloud instance app.get('/api/payment-metadata', async (req, res) => { try { - // Parse the URL to extract hostname and path - const url = new URL(config.app.sfPayments.metadataUrl) - // Use Node's https module instead of fetch - const data = await new Promise((resolve, reject) => { - const options = { - hostname: url.hostname, - path: url.pathname, - method: 'GET', - rejectUnauthorized: false, // This bypasses SSL verification - headers: { - Accept: 'application/json' - } - } - - const req = https.request(options, (response) => { - let data = '' - response.on('data', (chunk) => { - data += chunk - }) - response.on('end', () => { - try { - resolve(JSON.parse(data)) - } catch (e) { - reject(e) - } - }) - }) - - req.on('error', reject) - req.end() + const response = await fetch(config.app.sfPayments.metadataUrl, { + headers: { Accept: 'application/json' } }) - - // Transform relative icon paths to absolute URLs - const transformedData = transformIconPaths(data, url.hostname) - + if (!response.ok) { + throw new Error(`Metadata request failed with status: ${response.status}`) + } + const data = await response.json() + const transformedData = transformIconPaths(data, new URL(config.app.sfPayments.metadataUrl).hostname) res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Content-Type', 'application/json') res.json(transformedData) diff --git a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs index 50154adb65..c0f0673658 100644 --- a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs +++ b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs @@ -158,11 +158,14 @@ module.exports = { } }, // Salesforce Payments configuration - // Set enabled to false to disable Salesforce Payments even if the Commerce Cloud instance supports it. - // Example URLs: sdkUrl: 'https://.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js' - // metadataUrl: 'https://.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json' + // Set enabled to true to enable Salesforce Payments (requires the Salesforce Payments feature toggle to be enabled on the Commerce Cloud instance). + // Set enabled to false to disable Salesforce Payments on the storefront (the Commerce Cloud feature toggle is unaffected). + // sdkUrl and metadataUrl are hosted on your Commerce Cloud instance. Replace with your instance hostname. + // This may be a demandware.net hostname (e.g., myinstance.unified.demandware.net) or a vanity/custom hostname. + // sdkUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js' + // metadataUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json' sfPayments: { - enabled: true, + enabled: false, sdkUrl: '', metadataUrl: '' }, diff --git a/packages/template-retail-react-app/app/hooks/use-current-basket.js b/packages/template-retail-react-app/app/hooks/use-current-basket.js index bd83535fc9..fa553b26ac 100644 --- a/packages/template-retail-react-app/app/hooks/use-current-basket.js +++ b/packages/template-retail-react-app/app/hooks/use-current-basket.js @@ -95,8 +95,8 @@ export const useCurrentBasket = ({id = ''} = {}) => { ]) return { - data: currentBasket, ...restOfQuery, + data: currentBasket, derivedData: { // Only true if a non-temporary basket exists (temporary baskets are filtered out above) hasBasket: !!currentBasket, diff --git a/packages/template-retail-react-app/app/hooks/use-sf-payments.js b/packages/template-retail-react-app/app/hooks/use-sf-payments.js index 1edd672817..6ec5ceac37 100644 --- a/packages/template-retail-react-app/app/hooks/use-sf-payments.js +++ b/packages/template-retail-react-app/app/hooks/use-sf-payments.js @@ -43,6 +43,9 @@ export const useSFPayments = () => { } }, [status.loaded]) + const metadataUrl = config?.app?.sfPayments?.metadataUrl + const localEnabled = config?.app?.sfPayments?.enabled ?? true + const {data: serverMetadata, isLoading: serverMetadataLoading} = useQuery({ queryKey: ['payment-metadata'], queryFn: async () => { @@ -52,6 +55,9 @@ export const useSFPayments = () => { } return await response.json() }, + // Only fetch metadata if metadataUrl is set and sfPayments is enabled, + // prevents any 500 on server side and unnecessary network requests + enabled: localEnabled && !!metadataUrl, staleTime: 10 * 60 * 1000 // 10 minutes }) diff --git a/packages/template-retail-react-app/app/hooks/use-sf-payments.test.js b/packages/template-retail-react-app/app/hooks/use-sf-payments.test.js index ff3f9e490f..bc45e3c92c 100644 --- a/packages/template-retail-react-app/app/hooks/use-sf-payments.test.js +++ b/packages/template-retail-react-app/app/hooks/use-sf-payments.test.js @@ -127,7 +127,8 @@ describe('useSFPayments hook', () => { mockGetConfig.mockReturnValue({ app: { sfPayments: { - sdkUrl: 'https://test.sfpayments.com/sdk.js' + sdkUrl: 'https://test.sfpayments.com/sdk.js', + metadataUrl: 'https://test.sfpayments.com/metadata' } } }) @@ -298,6 +299,43 @@ describe('useSFPayments hook', () => { }) }) + describe('metadata query guard', () => { + test('does not fetch metadata when metadataUrl is empty', async () => { + mockGetConfig.mockReturnValue({ + app: { + sfPayments: { + sdkUrl: 'https://test.sfpayments.com/sdk.js', + metadataUrl: '' + } + } + }) + + renderWithQueryClient() + + await waitFor(() => { + expect(mockFetch).not.toHaveBeenCalled() + }) + }) + + test('does not fetch metadata when sfPayments is disabled', async () => { + mockGetConfig.mockReturnValue({ + app: { + sfPayments: { + enabled: false, + sdkUrl: 'https://test.sfpayments.com/sdk.js', + metadataUrl: 'https://test.sfpayments.com/metadata' + } + } + }) + + renderWithQueryClient() + + await waitFor(() => { + expect(mockFetch).not.toHaveBeenCalled() + }) + }) + }) + describe('confirming basket state', () => { test('startConfirming updates confirmingBasket', async () => { let hookData diff --git a/packages/template-retail-react-app/app/hooks/use-shopper-configuration.js b/packages/template-retail-react-app/app/hooks/use-shopper-configuration.js index e5df530857..cecc1ab859 100644 --- a/packages/template-retail-react-app/app/hooks/use-shopper-configuration.js +++ b/packages/template-retail-react-app/app/hooks/use-shopper-configuration.js @@ -13,7 +13,13 @@ import {useConfigurations} from '@salesforce/commerce-sdk-react' * @returns {*} The configuration value, or undefined if not found */ export const useShopperConfiguration = (configurationId) => { - const {data: configurations} = useConfigurations() + // Stale time is set to 10 minutes to avoid unnecessary API calls + const {data: configurations} = useConfigurations( + {}, + { + staleTime: 10 * 60 * 1000 // 10 minutes + } + ) const config = configurations?.configurations?.find( (configuration) => configuration.id === configurationId ) diff --git a/packages/template-retail-react-app/app/ssr.js b/packages/template-retail-react-app/app/ssr.js index 3048ec812d..5be0cdc8e5 100644 --- a/packages/template-retail-react-app/app/ssr.js +++ b/packages/template-retail-react-app/app/ssr.js @@ -19,7 +19,6 @@ import crypto from 'crypto' import express from 'express' import helmet from 'helmet' -import https from 'https' import {createRemoteJWKSet as joseCreateRemoteJWKSet, jwtVerify, decodeJwt} from 'jose' import path from 'path' import {getRuntime} from '@salesforce/pwa-kit-runtime/ssr/server/express' @@ -45,7 +44,6 @@ const options = { // The protocol on which the development Express app listens. // Note that http://localhost is treated as a secure context for development, // except by Safari. - // TODO: remove this and document instead in the release docs protocol: process.env.DEV_SERVER_PROTOCOL || 'http', // SSL file path for HTTPS development @@ -371,7 +369,8 @@ const {handler} = runtime.createHandler(options, (app) => { '*.stripe.com', '*.paypal.com', '*.adyen.com', - '*.google.com', + 'pay.google.com', + 'www.gstatic.com', '*.demandware.net', // Used to load a valid payment scripts in test environment 'maps.googleapis.com', 'places.googleapis.com' @@ -388,7 +387,11 @@ const {handler} = runtime.createHandler(options, (app) => { // Payment gateways '*.demandware.net', // Used to load a valid payment scripts in test environment '*.adyen.com', - '*.google.com' + '*.paypal.com', + 'pay.google.com', + 'payments.google.com', + 'google.com', + 'www.google.com' ], 'frame-src': [ // Allow frames from Salesforce site.com (Needed for MIAW) @@ -397,7 +400,8 @@ const {handler} = runtime.createHandler(options, (app) => { '*.stripe.com', '*.paypal.com', '*.adyen.com', - '*.google.com' + 'payments.google.com', + 'pay.google.com' ] } } @@ -458,50 +462,33 @@ const {handler} = runtime.createHandler(options, (app) => { // Helper function to transform relative icon paths to absolute URLs function transformIconPaths(data, ecomServerHost) { const baseUrl = `https://${ecomServerHost}/on/demandware.static/Sites-Site/-/-/internal` - const dataStr = JSON.stringify(data) - // Replace all relative icon paths with absolute URLs - const transformedStr = dataStr.replace(/"src":\s*"\/icons\//g, `"src":"${baseUrl}/icons/`) - return JSON.parse(transformedStr) + const methodTypes = data?.paymentMethodTypes + if (methodTypes) { + for (const method of Object.values(methodTypes)) { + for (const image of method.images ?? []) { + if (image.src?.startsWith('/icons/')) { + image.src = `${baseUrl}${image.src}` + } + } + } + } + return data } + // Helper function to fetch payment metadata from the Commerce Cloud instance app.get('/api/payment-metadata', async (req, res) => { try { - // Parse the URL to extract hostname and path - const url = new URL(config.app.sfPayments.metadataUrl) - // Use Node's https module instead of fetch - const data = await new Promise((resolve, reject) => { - const options = { - hostname: url.hostname, - path: url.pathname, - method: 'GET', - rejectUnauthorized: false, // This bypasses SSL verification - headers: { - Accept: 'application/json' - } - } - - const req = https.request(options, (response) => { - let data = '' - response.on('data', (chunk) => { - data += chunk - }) - response.on('end', () => { - try { - resolve(JSON.parse(data)) - } catch (e) { - reject(e) - } - }) - }) - - req.on('error', reject) - req.end() + const response = await fetch(config.app.sfPayments.metadataUrl, { + headers: {Accept: 'application/json'} }) - - // Transform relative icon paths to absolute URLs - const transformedData = transformIconPaths(data, url.hostname) - - res.setHeader('Access-Control-Allow-Origin', '*') + if (!response.ok) { + throw new Error(`Metadata request failed with status: ${response.status}`) + } + const data = await response.json() + const transformedData = transformIconPaths( + data, + new URL(config.app.sfPayments.metadataUrl).hostname + ) res.setHeader('Content-Type', 'application/json') res.json(transformedData) } catch (error) { diff --git a/packages/template-retail-react-app/config/default.js b/packages/template-retail-react-app/config/default.js index 270aa693f9..955212266c 100644 --- a/packages/template-retail-react-app/config/default.js +++ b/packages/template-retail-react-app/config/default.js @@ -88,8 +88,15 @@ module.exports = { }, storeLocatorEnabled: true, multishipEnabled: true, + // Salesforce Payments configuration + // Set enabled to true to enable Salesforce Payments (requires the Salesforce Payments feature toggle to be enabled on the Commerce Cloud instance). + // Set enabled to false to disable Salesforce Payments on the storefront (the Commerce Cloud feature toggle is unaffected). + // sdkUrl and metadataUrl are hosted on your Commerce Cloud instance. Replace with your instance hostname. + // This may be a demandware.net hostname (e.g., myinstance.unified.demandware.net) or a vanity/custom hostname. + // sdkUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js' + // metadataUrl: 'https:///on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json' sfPayments: { - enabled: true, + enabled: false, sdkUrl: '', metadataUrl: '' }, diff --git a/packages/template-retail-react-app/package.json b/packages/template-retail-react-app/package.json index 90e2461053..0553ef24a6 100644 --- a/packages/template-retail-react-app/package.json +++ b/packages/template-retail-react-app/package.json @@ -101,7 +101,7 @@ "bundlesize": [ { "path": "build/main.js", - "maxSize": "98 kB" + "maxSize": "102 kB" }, { "path": "build/vendor.js",