Skip to content

feat(payment): PAYPAL-6499 add wallet buttons#3062

Open
bc-yaroslav-zhmutskyi wants to merge 1 commit into
canaryfrom
PAYPAL-6499
Open

feat(payment): PAYPAL-6499 add wallet buttons#3062
bc-yaroslav-zhmutskyi wants to merge 1 commit into
canaryfrom
PAYPAL-6499

Conversation

@bc-yaroslav-zhmutskyi

@bc-yaroslav-zhmutskyi bc-yaroslav-zhmutskyi commented Jun 24, 2026

Copy link
Copy Markdown

What/Why?

Add wallet button integration for cart page

Testing

Manual

video_paypal.mp4

Migration

No need

@bc-yaroslav-zhmutskyi
bc-yaroslav-zhmutskyi requested a review from a team as a code owner June 24, 2026 08:59
@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fb2e096

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bigcommerce/catalyst-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview, Comment Jul 9, 2026 1:10pm

Request Review

@bc-yaroslav-zhmutskyi
bc-yaroslav-zhmutskyi marked this pull request as draft June 24, 2026 08:59
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Report

Comparing against baseline from 2b7f2cc (2026-07-09).

Metric Baseline Current Delta
Total JS 446.1 kB 447.1 kB +1 kB (+0.2%)

Per-Route First Load JS

Route Baseline Current Delta
/(default)/cart/page 347.2 kB 348.3 kB +1.1 kB (+0.3%)

Threshold: 5% increase. Routes with ⚠️ exceed the threshold.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Unlighthouse Performance Comparison — Vercel

Comparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores.

Summary Score

Aggregate score across all categories as reported by Unlighthouse.

Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Score 89 92 92 94

Category Scores

Category Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Performance 74 83 74 69
Accessibility 95 92 95 95
Best Practices 100 100 100 100
SEO 88 100 100 100

Core Web Vitals

Metric Prod Desktop Prod Mobile Preview Desktop Preview Mobile
LCP 3.7 s 4.7 s 3.7 s 5.7 s
CLS 0.049 0 0.001 0.19
FCP 1.1 s 1.1 s 1.2 s 1.2 s
TBT 10 ms 0 ms 10 ms 0 ms
Max Potential FID 60 ms 40 ms 70 ms 50 ms
Time to Interactive 3.7 s 4.7 s 6.1 s 5.7 s

Full Unlighthouse report →

Comment thread core/.env.example
# Disable automatic fetch instrumentation (not recommended unless using custom instrumentation)
# NEXT_OTEL_FETCH_DISABLED=1

# Checkout SDK loader URL used in local development for wallet buttons.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that the related files should be places in the core/public/v1 folder to be able use them via next server in dev mode

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added info

@bc-nick

bc-nick commented Jul 3, 2026

Copy link
Copy Markdown

Also can you please provide some information regarding how the release environment works here? How the release flow should be managed in our case? Probably it should be covered by exp or something before merging.

@bc-yaroslav-zhmutskyi

Copy link
Copy Markdown
Author

Also can you please provide some information regarding how the release environment works here? How the release flow should be managed in our case? Probably it should be covered by exp or something before merging.

Our changes will be covered by experiment on backend side storefront service and will be disabled in prod for now.
Canary branch containing the latest code in development and the most recent commit may not be stable. After merge of this PR we will need to create release

@bc-nick bc-nick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGMT

Comment thread core/lib/currency.ts
export async function getPreferredCurrencyCode(code?: string): Promise<CurrencyCode | undefined> {
const cookieStore = await cookies();
const currencyCode = cookieStore.get('currencyCode')?.value;
const currencyCode = cookieStore.get('currencyCode')?.value || code;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cookie || code means the cookie always overrides the code arg, so getCurrencyData(cart.currencyCode) can get silently overridden by a stale currency cookie. Cart currency's fixed at creation, so this can mismatch amount vs currency on the wallet button. I would say leave this helper's signature alone (~20 other call sites) and just do (await getPreferredCurrencyCode()) ?? currencyCode inline in getCurrencyData.

Comment on lines +16 to +29
const isMountedRef = useRef(false);
const initButtonProps = useStreamable(walletButtonsInitOptions);

useEffect(() => {
if (!isMountedRef.current && initButtonProps.length) {
isMountedRef.current = true;

const initWalletButtons = async () => {
await new WalletButtonsInitializer().initialize(initButtonProps);
};

void initWalletButtons();
}
}, [cartId, initButtonProps]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMountedRef makes this a one-shot init forever. Deps [cartId, initButtonProps] say "re-run on change," but the ref blocks that after the first run, so a quantity change won't update the PayPal amount. I would fix this before merge.

Comment on lines +21 to +27
isMountedRef.current = true;

const initWalletButtons = async () => {
await new WalletButtonsInitializer().initialize(initButtonProps);
};

void initWalletButtons();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMountedRef gets set before initialize() resolves, and void initWalletButtons() has no .catch. If the SDK script fails to load, that rejection's swallowed and it never retries, wallet buttons just silently never show up. Worth a .catch that resets the ref.

cartId: string;
}) => {
const isMountedRef = useRef(false);
const initButtonProps = useStreamable(walletButtonsInitOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs outside the <Stream fallback={null}> boundary below, so a rejected promise throws into render instead of degrading gracefully like the fallback implies. Might be worth moving the unwrap inside Stream's render prop.

{(buttonOptions) => (
<div style={{ display: 'flex', alignItems: 'end', flexDirection: 'column' }}>
{buttonOptions.map((button) =>
button.containerId ? <div id={button.containerId} key={button.containerId} /> : null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: containerId is required and always set, so : null here is dead. Could just be buttonOptions.map((button) => <div id={button.containerId} key={button.containerId} />).

type PaymentWalletsVariables = VariablesOf<typeof PaymentWalletsQuery>;

export const getPaymentWallets = async (variables: PaymentWalletsVariables) => {
const customerAccessToken = await getSessionCustomerAccessToken();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSessionCustomerAccessToken isn't cache()'d, so with N wallets it decodes the session N+2 times per render. Might be worth wrapping it in cache().

Comment thread core/proxy.ts
* - robots.txt (robots route)
*/
'/((?!api|admin|_next/static|_next/image|_vercel|favicon.ico|xmlsitemap.php|sitemap.xml|robots.txt).*)',
'/((?!api|admin|_next/static|_next/image|v1|_vercel|favicon.ico|xmlsitemap.php|sitemap.xml|robots.txt).*)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This v1 isn't segment-anchored, so it'll match any slug starting with "v1" (e.g. /v1-turbo-kit), not just the loader path. Might want v1/ instead.

GiftCertificateCodeFormState,
} from '@/vibes/soul/sections/cart/gift-certificate-code-form';
import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';
import { ClientWalletButtons } from 'components/wallet-buttons';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things: this import should be ~/components/wallet-buttons like the rest of the file. Also, vibes/soul's supposed to stay presentation-only, this pulls in app-level wallet logic plus a cartId prop. Could pass ClientWalletButtons in as a ReactNode slot instead (see product-detail's additionalActions).

const checkoutButtonModule = await window.checkoutKitLoader.load('wallet-button');

return checkoutButtonModule.createWalletButtonInitializer({
graphQLEndpoint: '/graphql',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should react to the trailing slash environment variable and append it if it's enabled.

Comment thread core/.env.example
# Checkout SDK loader URL used in local development for wallet buttons.
# In non-development environments the production CDN loader is always used.
# Related files can be placed in core/public/v1 so they are served by the Next server in dev mode
NEXT_PUBLIC_CHECKOUT_SDK_DEV_URL=http://localhost:3000/v1/loader.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid having to drop the file in the local Catalyst folder? My thoughts here is that since you are trying to make changes in both repos, you can have two separate running dev processes:

  1. In Catalyst, http://localhost:3000/...
  2. In checkout-sdk-js, http://localhost:3001/v1/loader.js

This way we can get rid of the proxy.ts change below, you can make changes in both repos without having to rebuild, we don't need to drop a file into core/public/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants