W-21432256: Salesforce Payments on PWA Feature#3709
W-21432256: Salesforce Payments on PWA Feature#3709amittapalli wants to merge 93 commits intodevelopfrom
Conversation
* PWA on SFP spike * sfp on pwa part 1 * use new payment instrument request * conditionally render express box * add ShopperPayments tests * lint and tests * address comments * clean up --------- Co-authored-by: Jeff Raab <jraab@salesforce.com>
* render paypal * refactor shopper config checker
* refactor shopper config checker * baskets v2 temp commit * complete paypal and code clean up * address comments * fix
* @W-19685609 Express on PDP with Temporary Basket * Address Code Review: Fix comments, remove eagerly created validations, move temp basket to its a hook, i10n for errors, etc * Address Code Review: Fix additional comments, i10 labels, set keepPreviousData to false by default in current basket hook * Address Code Review: do not set keepPreviousData flag to true in useCurrentBasket hook until further testing confirms that it is needed * Address updates to SDK event changes in payment sheet * Remove the optional keepPreviousData property setting in usecurrentbasket hook * Undo i10 call for an error that nees to be looged into console only * Reset maximumButtonCount to 1 * Remove commented line * Replace undefined with empty function for onclick action in payment sheet
…/sfp-update-amount-prevent-reinit @W-20117176 : Call updateAmount on Order Total change
* W-19443266: Handle Failure use-cases by calling Fail Order where necessary. Also refactor the client-secret retrieval since the response structure has changed.
…/order-confirmation-hide-sfp-payment-details @W-19799923: Remove payment details on order confirmation page for SFP orders
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…/fail-order-on-redirect W-21372336: Fail Order for redirect based payments
…/stripe-caching-for-display-spm-fix-tests W-21411273, W-21434212: Fix for Display SPMs at checkout due to caching issues and Fix for failing tests on feature branch
vmarta
left a comment
There was a problem hiding this comment.
Still reviewing the PR. Posting these comments I have so far..
| sfPayments: { | ||
| enabled: true, | ||
| sdkUrl: '', | ||
| metadataUrl: '' | ||
| }, |
There was a problem hiding this comment.
Should sf payments integration be enabled by default? It won't work with the empty sdkUrl and metadataUrl anyways.
When I run the template as is, the checkout is still operating.. it falls back to the traditional checkout. How about if we set enabled=false initially? Once the users finish the configuration, they should have already set the enabled=true.
There was a problem hiding this comment.
@vmarta, so its a combination type of switch. Even if sfPayments.enabled=true in pwakit, the API returns the feature toggle value. When both are true, then its enabled. If either one of them are false, its not enabled.
Do you prefer that its always false and users have to enable it again in PWA even if FT is true?
Which ODS are you using since SF Payments needs to be configured as well. I wasn't sure how to add the value to the sdkUrl and metadataUrl and thought maybe we just document that. The handlebar file has the comments and I can add the comments here too.
So, for any internal ODS, it will look like this but someone told me that in production vanity urls can be used. So, I thought maybe I need to leave it empty and at most add comments. But we can discuss further tomorrow
sdkUrl: 'https://zyoe-011.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js',
metadataUrl:
'https://zyoe-011.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/metadata/v1.json'
There was a problem hiding this comment.
@vmarta So, this was the mental model I had and please let me know if this is not how it should work
If I leave it to false, then every customer who wants sf payments needs to enable it in 2 different places (in feature toggle and in pwa)
If I leave it to true, then customer has the capability to use sf payments but it will only be visible if the FT is enabled by merchant
If I remove this flag completely and then depend on just the FT, then a customer has no kill switch to disable their instance if they wanted to.
There was a problem hiding this comment.
Thanks for the explanation. The mental model that you've just written should be what's in the comment. It's much clearer :) Yes, please update the comment in all the config files (both in template and handlebars).
Having said that, let me ask you another question: if sf payment is enabled by default, let's say the FT is also enabled. Would the feature still work if the sdkUrl is empty?
I believe it won't work, and that's why I wish that it's enabled=false by default. When the users enter their sdkUrl value, that's when they flip the enabled to true.
There was a problem hiding this comment.
Yes, I set it to false and added a bit more comments
| export const useCurrentBasket = ({id = ''} = {}) => { | ||
| const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED | ||
| const customerId = useCustomerId() | ||
| const {confirmingBasket} = useSFPayments() |
There was a problem hiding this comment.
What happens if a project does not use sf payments feature? How costly is this call to useSFPayments for every call to useCurrentBasket?
Please consider changes to useCurrentBasket to be mindful of those users not using sf payment.
There was a problem hiding this comment.
Let me take a look again. Most of the stuff in useSFPayments hook are lightweight BUT useQuery for payment-metadata can be more expensive. It makes a fetch to /api/payment-metadata, even when SF Payments isn't configured. That call actually is made from ssr.js. But even though the fetch happens during SSR (so it's not a client-side network cost), it's still an unnecessary server-side fetch during the SSR process for every page render on projects that don't use SF Payments. That adds latency to the server-side render for no reason. It might not be as bad as client-side. What we can do is perhaps add some kind of short circuit in the useSFPayments hook, basically check if payments is enabled (config flag = true and api value = true) and then only process it. Will need to test it out
| } | ||
| const subscribers = new Set() | ||
|
|
||
| export const useSFPayments = () => { |
There was a problem hiding this comment.
Related to the previous comment, calling useSFPayments hook with empty configuration results in noisy errors logged in the browser console.
useQuery for /api/payment-metadata has no enabled guard, so it fires on every page load even when sdkUrl/metadataUrl are empty
(the default). The SSR proxy then tries new URL('') on the empty metadataUrl and returns a 500 — harmless to the user but noisy in the
console.
The other two side effects are already self-gating:
useScript(sdkUrl)— hasif (!src) { return }internally, no script tag injecteduseEffectfor SDK init — gated onstatus.loaded && typeof window.SFPayments === 'function', never runs if script didn't load
Only useQuery needs a guard. Suggested fix in use-sf-payments.js:
export const useSFPayments = () => {
const appOrigin = useAppOrigin()
const config = getConfig()
const sdkUrl = config?.app?.sfPayments?.sdkUrl
+ const metadataUrl = config?.app?.sfPayments?.metadataUrl
const status = useScript(sdkUrl)
...
const {data: serverMetadata, isLoading: serverMetadataLoading} = useQuery({
queryKey: ['payment-metadata'],
queryFn: async () => {
const response = await fetch(`${appOrigin}/api/payment-metadata`)
if (!response.ok) {
throw new Error('Failed to load payment metadata')
}
return await response.json()
},
- staleTime: 10 * 60 * 1000 // 10 minutes
+ staleTime: 10 * 60 * 1000, // 10 minutes
+ enabled: !!metadataUrl
})Using URL presence as the condition (rather than localEnabled) is more precise — enabled: true with empty URLs is still an unconfigured
state, so there's nothing to fetch regardless of the flag. A true early return isn't possible here due to Rules of Hooks.
There was a problem hiding this comment.
Ya, was planning on making use of enabled property as well
| '*.stripe.com', | ||
| '*.paypal.com', | ||
| '*.adyen.com', | ||
| '*.google.com', |
There was a problem hiding this comment.
This matches a lot of things. Let's consider a tighter scope for this domain. Perhaps pay.google.com.
| '*.paypal.com', | ||
| '*.adyen.com', | ||
| '*.google.com', | ||
| '*.demandware.net', // Used to load a valid payment scripts in test environment |
There was a problem hiding this comment.
Can we tighten this too please just to be safe? Where is the payment script coming from... *.unified.demandware.net ?
There was a problem hiding this comment.
With CSP urls, it turns out that we can specify the path too.
The comment on *.demandware.net in script-src says "used to load valid payment scripts in test environment." The actual SDK URL is known
from the config comments:
https://.unified.demandware.net/on/demandware.static/Sites-Site/-/-/internal/jscript/sfp/v1/sfp.js
So this could be tightened from:
- '*.demandware.net'
- '*.demandware.net/on/demandware.static/'
Similarly for connect-src and img-src. That still uses a wildcard subdomain (unavoidable since it's instance-specific) but at least limits
execution to the static file path rather than the entire domain.
The frame-src entries (*.stripe.com, *.paypal.com, etc.) can't be tightened with paths — that's a spec limitation, not a PR issue.
One More Caveat
Path restrictions are bypassed by redirects — if a script at an allowed path issues a redirect to a disallowed path, some browsers will
still allow it. So path scoping is defense-in-depth, not a hard guarantee.
| // TODO: remove this and document instead in the release docs | ||
| protocol: process.env.DEV_SERVER_PROTOCOL || 'http', |
There was a problem hiding this comment.
What do we want to re: this TODO?
There was a problem hiding this comment.
Ya, removed that TODO
| hostname: url.hostname, | ||
| path: url.pathname, | ||
| method: 'GET', | ||
| rejectUnauthorized: false, // This bypasses SSL verification |
There was a problem hiding this comment.
Could you explain more why bypassing ssl verification is needed? At first glance, this sounds like something we shouldn't do.
There was a problem hiding this comment.
hmm I think I might be able to replace this with a fetch call instead of using http
| 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) | ||
| } |
There was a problem hiding this comment.
This function converts data into json string and then back to data. Do we really need to convert it to json first? If we know the data structure, then we can query the specific key and replace its value with the absolute url.
There was a problem hiding this comment.
ya, you are right. I changed it
| return JSON.parse(transformedStr) | ||
| } | ||
|
|
||
| app.get('/api/payment-metadata', async (req, res) => { |
There was a problem hiding this comment.
No authentication on this endpoint — it's publicly accessible. Is that all right?
There was a problem hiding this comment.
right, you should be able to navigate to it from the BM instance since its just metadata
| // Transform relative icon paths to absolute URLs | ||
| const transformedData = transformIconPaths(data, url.hostname) | ||
|
|
||
| res.setHeader('Access-Control-Allow-Origin', '*') |
There was a problem hiding this comment.
This is not needed because the request origin is the same.
…/merge-changes-from-develop W-21436534: Merge from Develop to feature branch
| export const useSFPaymentsEnabled = () => { | ||
| const config = getConfig() | ||
| const localEnabled = config?.app?.sfPayments?.enabled ?? true | ||
| const apiEnabled = useShopperConfiguration('SalesforcePaymentsAllowed') === true |
There was a problem hiding this comment.
This SCAPI request can be expensive, especially when the backend configuration should not change that often.
On the other hand, SCAPI requests like this is cached by Tanstack Query library. But by default, it's cached only for 10 seconds.
There's the answer. The global default is 10 seconds:
// _app-config/index.jsx
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
staleTime: 10 * 1000, // ← 10 seconds
}
}
So useConfigurations — which backs useSFPaymentsEnabled — goes stale after 10 seconds. Any component mounting after that triggers a
background refetch to the Configurations API. On most page navigations that take longer than 10 seconds, it refetches.
The inconsistency with the rest of the sfPayments hooks is noticeable:
┌──────────────────────────────────────────────┬─────────────────────────────┐
│ Query │ staleTime │
├──────────────────────────────────────────────┼─────────────────────────────┤
│ useConfigurations (via useSFPaymentsEnabled) │ 10s (global default, unset) │
├──────────────────────────────────────────────┼─────────────────────────────┤
│ payment-metadata (in useSFPayments) │ 10 minutes (explicit) │
├──────────────────────────────────────────────┼─────────────────────────────┤
│ use-sf-payments-country.js │ 30 minutes (explicit) │
└──────────────────────────────────────────────┴─────────────────────────────┘
Configuration data like SalesforcePaymentsAllowed is a merchant-level setting that essentially never changes at runtime. It should carry a
much longer staleTime, consistent with how the rest of the sfPayments hooks treat their data. useShopperConfiguration could pass a
queryOptions argument through to useConfigurations:
export const useShopperConfiguration = (configurationId) => {
const {data: configurations} = useConfigurations(
{},
{staleTime: 30 * 60 * 1000} // treat config as long-lived
)
...
}
Or set it at the useSFPaymentsEnabled call site via the second queryOptions argument that useShopperConfiguration currently doesn't expose.
Either way, leaving it at the 10-second global default means the Configurations endpoint gets hit on virtually every page load for all
users.
There was a problem hiding this comment.
See this comment for a possible way to address the above caching: #3709 (comment)
There was a problem hiding this comment.
I added a cache time for now for that userShopperConfig call
| export const useShopperConfiguration = (configurationId) => { | ||
| const {data: configurations} = useConfigurations() | ||
| const config = configurations?.configurations?.find( | ||
| (configuration) => configuration.id === configurationId | ||
| ) | ||
| return config?.value | ||
| } |
There was a problem hiding this comment.
Oh I see useShopperConfiguration is a custom hook that is defined in the template (not in commerce-sdk-react). So we can tweak it to allow additional options like staleTime.
-export const useShopperConfiguration = (configurationId) => {
- const {data: configurations} = useConfigurations()
+export const useShopperConfiguration = (configurationId, queryOptions = {}) => {
+ const {data: configurations} = useConfigurations(undefined, queryOptions)
const config = configurations?.configurations?.find(
(configuration) => configuration.id === configurationId
)
return config?.value
}
Then the caller decides the caching behaviour — useSFPaymentsEnabled would become:
export const useSFPaymentsEnabled = () => {
const config = getConfig()
const localEnabled = config?.app?.sfPayments?.enabled ?? true
const apiEnabled =
useShopperConfiguration('SalesforcePaymentsAllowed', {
staleTime: 30 * 60 * 1000
}) === true
return localEnabled && apiEnabled
}
This keeps the hardcoded value out of the hook implementation, lets other callers (useAutomaticCapture, useFutureUsageOffSession) set their
own staleTime if needed, and aligns exactly with the pattern already used everywhere else in the package.
* W-21432256: address code review comments from PWA team
…/payment-sheet-zone-id W-21324157: Zone ID updated to use paymentconfig zoneID in Payment Sheet
…-20911530-UT W-20911530: fixing failing unit test
…ommerceCloud/pwa-kit into t/team404/sfp-on-pwa merging
vmarta
left a comment
There was a problem hiding this comment.
Thanks Anitha for addressing all the feedback. Just one small comment below.
| 'google.com', | ||
| 'www.google.com' |
There was a problem hiding this comment.
For what reasons do we need these domains? We already have pay.google.com and payments.google.com above.
There was a problem hiding this comment.
Removing google.com / www.google.com can very likely break the Google Pay flow, even though the button itself still renders. There tend to be 3DS and other redirections that google pay does during the flow and we need to make sure that none of those break. Thats why we left them in connect source but not in script source.
I saw these in the browser
Connecting to 'https://google.com/pay' violates the following Content Security Policy directive: "connect-src ..."
This means the Google Pay script successfully loads, but when it tries to communicate with Google’s payment service, the request to google.com is blocked by CSP.
We can do some thorough testing after the merge and once its in the right environments and discuss it with others in the team before updating it further
There was a problem hiding this comment.
@amittapalli we can update it to be google.com/pay/ then. As this previous comment said, we can control the path too.. not just the domains.
After the merge, please update it to be more narrow scoped.
joeluong-sfcc
left a comment
There was a problem hiding this comment.
Minor comments, biggest call out is types.ts, everything else is secondary
| */ | ||
| export interface ApiClients { | ||
| shopperBaskets?: ShopperBaskets<ApiClientConfigParams> | ||
| shopperBasketsV2?: ShopperBaskets<ApiClientConfigParams> |
There was a problem hiding this comment.
| shopperBasketsV2?: ShopperBaskets<ApiClientConfigParams> | |
| shopperBasketsV2?: ShopperBasketsV2<ApiClientConfigParams> |
| @@ -0,0 +1,9 @@ | |||
| /* | |||
| * Copyright (c) 2023, Salesforce, Inc. | |||
There was a problem hiding this comment.
NIT, for all comment headers in the new ShopperBasketsV2 directory:
| * Copyright (c) 2023, Salesforce, Inc. | |
| * Copyright (c) 2026, Salesforce, Inc. |
|
|
||
| /** | ||
| * Mutations available for Shopper Baskets. | ||
| * @group ShopperBaskets |
There was a problem hiding this comment.
In both mutation.ts and query.ts there we should update all typedoc comments
| * @group ShopperBaskets | |
| * @group ShopperBasketsV2 |
| // Most test cases only apply to non-empty response test cases, some (error handling) can include deleteBasket | ||
| const allTestCases = [...nonEmptyResponseTestCases, ...emptyResponseTestCases] | ||
|
|
||
| describe('ShopperBaskets mutations', () => { |
There was a problem hiding this comment.
Let's differentiate V1 and V2 when we run our unit tests, we should apply this change in all the new test files in the ShopperBasketsV2 directory if applicable, based on my search we should update:
- index.test.ts
- mutation.test.ts
- query.test.ts
| describe('ShopperBaskets mutations', () => { | |
| describe('ShopperBasketsV2 mutations', () => { |
| '/commerce-sdk-react', | ||
| '/organizations/', | ||
| string | undefined, | ||
| '/baskets/', |
There was a problem hiding this comment.
Currently with this version of queryKeyHelpers.ts, If a developer uses both useBasket (V1) and useBasketV2 (V2) during a migration period, they will share the exact same TanStack Query cache entry. A V2 mutation will update/invalidate V1 query caches and vice versa. I'm not sure how likely this scenario will happen where a customer mixes and matches V1 and V2, but to be on the safe side and to decouple V1 and V2 completely, we should update this implementation so that V2 endpoints target different cache keys than V1 endpoints:
| '/baskets/', | |
| '/baskets/v2', |
For all keys
There was a problem hiding this comment.
We can take a look at this post merge. How likely is it that someone uses V1 and V2 simultaneously? If it's extremely unlikely and this is a time-sensitive merge, it might be something to address in a follow-up
|
This PR has been squash merged to |
This PR spans 3 packages with ~15,165 lines added across 120 files (including tests and translations). The changes enable Stripe, Adyen, and PayPal payment processing via the SF Payments SDK, with both standard checkout and express payment flows (Apple Pay, Google Pay, PayPal, Venmo).
Gus item: https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00002VgsrOYAR/view
Description
This document summarizes the Salesforce Payments on PWA Feature:
https://salesforce.quip.com/UErWA4QiaiES
Areas that are common and can be reviewed further include
Areas that are Domain/Payments specific
Types of Changes
Changes
How to Test-Drive This PR
Checklists
General
Accessibility Compliance
You must check off all items in one of the follow two lists:
or...
Localization