Skip to content

feat(payment): POC headless wallet buttons implementation #1999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: canary
Choose a base branch
from

Conversation

bc-nick
Copy link

@bc-nick bc-nick commented Feb 26, 2025

What/Why?

POC of Headless Wallet Buttons feature

POC related to checkout-sdk part: bigcommerce/checkout-sdk-js#2742

Testing

headless_wallet_buttons_with_catalyst_sf.mov

Copy link

changeset-bot bot commented Feb 26, 2025

⚠️ No Changeset found

Latest commit: 5759535

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Copy link

vercel bot commented Feb 26, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
catalyst-canary ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 24, 2025 8:36pm
4 Skipped Deployments
Name Status Preview Comments Updated (UTC)
catalyst ⬜️ Ignored (Inspect) Mar 24, 2025 8:36pm
catalyst-au ⬜️ Ignored (Inspect) Visit Preview Mar 24, 2025 8:36pm
catalyst-soul ⬜️ Ignored (Inspect) Visit Preview Mar 24, 2025 8:36pm
catalyst-uk ⬜️ Ignored (Inspect) Visit Preview Mar 24, 2025 8:36pm

@bc-nick bc-nick changed the title feat(payment): PAYPAL-5114 headless wallet buttons implementation feat(payment): PAYPAL-5114 POC headless wallet buttons implementation Feb 27, 2025
@bc-nick
Copy link
Author

bc-nick commented Feb 27, 2025

I'd like to provide some information:

  1. core/public/v1 is the place where we will unload checkout-sdk build if we wont to develop locally, for this we need to run npm run build-cdn command and move all build files from dist-cdn/v1 folder to core/public/v1. For prod we are going to use CDN, here is this place

  2. on the screen record in description we are not able redirect to checkout page properly, we are working on it now, this is an back-end issue

  3. checkout-sdk part is not released yet, but in progress of implementation, POC of this part of work you can check here (build with this changes is using in this project)

Based on this PR will be created appropriate tickets when checkout-sdk part will be finished and released

@animesh1987
Copy link

Hey @bc-nick are we going to copy paste all of sdk dist file code here?

Can we otherwise fetch the SDK button script from CDN?

@bc-nick
Copy link
Author

bc-nick commented Feb 27, 2025

Hey @bc-nick are we going to copy paste all of sdk dist file code here?

Can we otherwise fetch the SDK button script from CDN?

@animesh1987 it will be enough to copy paste only required files for local developing: loader.js and headless-checkout-wallet-....js

Comment on lines 36 to 53
if (request.url.includes('/cart-information') && request.method === 'GET') {
const cartId = request.nextUrl.searchParams.get('cartId');

if (cartId) {
try {
const cart = await fetchCart(cartId);

return NextResponse.json(cart);
} catch (error) {
return NextResponse.json({ error });
}
}

return NextResponse.json(
{ error: 'Invalid request body: cartId is not provided' },
{ status: 400 },
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

After chatting offline, we should remove this entire middleware handler and move these blocks into route handlers.

For example, this block should be moved to core/app/api/(wallet-buttons)/cart-information/route.ts:

import { NextRequest, NextResponse } from 'next/server';

export const GET = async (request: NextRequest) => {
  const cartId = request.nextUrl.searchParams.get('cartId');

  if (cartId) {
    try {
      const cart = await fetchCart(cartId);

      return NextResponse.json(cart);
    } catch (error) {
      return NextResponse.json({ error });
    }
  }

  return NextResponse.json(
    { error: 'Invalid request body: cartId is not provided' },
    { status: 400 },
  );
};

Comment on lines 191 to 242
{walletButtons && (
<div className="mt-4">
<ClientWalletButtons walletButtons={walletButtons} />
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

@migueloller @hunterbecton, do you think this is the right API to consume the wallet buttons in Vibes?

Copy link
Contributor

Choose a reason for hiding this comment

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

As chatted about offline, these files are only for local development only. We will not need these files until bigcommerce/checkout-sdk-js#2742 is merged and deployed to the CDN. Leaving this comment to ensure we remove these files before merge.

Copy link
Author

Choose a reason for hiding this comment

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

Sure, I will remove this files

Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to comment on all the little changes I wanted in this core/lib/wallet-buttons but it got pretty long. Instead I have a branch of my vision of what I wanted to change PAYPAL-5114...chore/headless-wallets-cleanup

The main subjects:

  • Reduce the amount of indirections (too many files) to complete the task.
    • Passed the only information needed into the initializer (methodIds) to reduce surface area
  • Simplify the WalletButtonsInitializer class and remove any utilities + simplify types.
    • Only thing needed by the constructor was the url.
    • Moved the loader script URL into a private attribute.
  • Add a union of types for the different payment options. In practice, this usually helps remove a lot of conditional type checking in application code.
  • Reject initializing if the script element fails onerror + onabort.
  • Proper gql.tada usage, instead of using string template literals to inject dynamic data.
  • Removed additions to core/client/queries – when you remove items from middleware, just place the fetching in the same file as the route handlers.

Copy link
Author

Choose a reason for hiding this comment

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

thanks for the recommendations, I will take a look tomorrow and come back as soon as possible

Copy link
Author

Choose a reason for hiding this comment

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

I have made some updates regarding comments related to most of them

const graphQLQuery = graphql(`
query {
site {
paymentWalletWithInitializationData(filter: {paymentWalletEntityId: "${entityId}"}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not the proper way to pass in data in gql.tada queries, check around the codebase to see how to do it properly or check out this example PAYPAL-5114...chore/headless-wallets-cleanup#diff-d5e5c3bd2d9602afaf35db11c6adea51a6a29268df218fe0e76e3035a31d57c5R200-R227.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you check out the docs and experiment on how to properly add route handlers. I want to completely get rid of this middleware and route handlers solve that.

Copy link
Contributor

Choose a reason for hiding this comment

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

This server component file is not even being used. Let's just move all the code from core/components/wallet-buttons/_components/client-wallet-buttons/index.tsx into this file.

Copy link
Author

Choose a reason for hiding this comment

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

This file is not used yet. But I left it in order to have opportunity render this component through server side. But I can remove it of course

Copy link
Contributor

Choose a reason for hiding this comment

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

Since checkout-sdk is client-side only, it doesn't really make sense to keep a server component.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is not really a server action. Like in my example, can we just move this into page-data.ts and/or inline some of this in the page.tsx file (see my branch).

Copy link
Author

Choose a reason for hiding this comment

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

I will add this changes and let you know, thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

All of these additional queries that were added can just be inlined inside the route handlers once you have them set up correctly.

Copy link
Author

Choose a reason for hiding this comment

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

Got you, thanks

Copy link
Author

Choose a reason for hiding this comment

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

Not all of them. This request will not be moved because it is using in another place.

I have removed middleware and switched to route api.

Copy link
Author

Choose a reason for hiding this comment

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

@chanceaclark do we really need to move all queries to the route handlers? It we do, I would like to suggest create a file for each of them app/api/wallet-buttons/.../query.ts to avoid a huge amount of code accumulating in one place. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just inline in each of the route handlers for now, we can optimize/move them later if it deems necessary.


const cart = response.data.site.cart;
Copy link
Contributor

Choose a reason for hiding this comment

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

This change seems unnecessary. Can we just destructure here instead of splitting the function in two separate functions? I understand you are trying to fetch the cart separately, but it looks like you just need the carts currency code. I would just create a separate query for the currency code, more inlined with the route handler that needs it.

const { cart, checkout } = response.data.site; 

Copy link
Contributor

Choose a reason for hiding this comment

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

Why did we need to change this file? Seems like something we should avoid.

Copy link
Author

Choose a reason for hiding this comment

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

Because we do not need to work with form validation and with cart action in our case. We need to make a request for getting information about redirect_urls only and pass it to checkout-sdk.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd avoid changing the original function and make a separate function for what you need. Abstracting things is not always the best approach and it's fine to be duplicative at first.

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason why my branch didn't have this util file is that it's a bit of a indirection. We should keep the API of WalletButtonsInitializer pretty slim, which means that it only needs to know about the methodIds that are needed. We can return the Option interface to the consumer to handle what it needs to.

The indirections I see:

-> fetch methods (app)
-> getWalletButtonOptions (lib)
-> pass to wallet buttons component (app)
-> return options (lib)
-> render components (app)

However, we can simplify this:

-> fetch methods (app)
-> pass to wallet buttons component (app)
-> initialize options (lib)
-> render components (app)

It also means we don't have to handle the undefined case in our app, which adds complexity. We should handle this inside the lib and only return the type Option[]

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately this filter will not work correctly because the id's are different. But I think you are right about your approach. I will make some updates providing approach based on your's suggested and will let you know

Copy link
Contributor

Choose a reason for hiding this comment

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

What you have I think is way more complex than it should be. If the id's are different you can just change this data structure to be a record instead, then your initialize function returns an array:

const options: Record<string, Option> = {
  'paypalcommerce.paypal': {
    methodId: 'paypalcommerce',
    containerId: 'paypalcommerce-button',
    paypalcommerce: {
      style: { color: 'gold', label: 'checkout' },
      cartId: 'cartId',
    },
  },
  // ...
};

// ...
async initialize(methodIds: string[]): Promise<Option[]> {
  await this.initializeCheckoutKitLoader();

  const checkoutButtonInitializer = await this.initCheckoutButtonInitializer();

  return methodIds
    .map(methodId => options[methodId])
    .filter(Boolean)
    .map((option) => {
      checkoutButtonInitializer.initializeHeadlessButton(option);

      return option;
    });
}

Copy link
Author

Choose a reason for hiding this comment

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

@chanceaclark I created class that works with that structure and return config with available wallet button options. This options may be different for each of the available buttons.

Copy link
Contributor

⚡️🏠 Lighthouse report

Lighthouse ran against https://catalyst-canary-fggvp8r1u-bigcommerce-platform.vercel.app

🖥️ Desktop

We ran Lighthouse against the changes on a desktop and produced this report. Here's the summary:

Category Score
🟢 Performance 96
🟢 Accessibility 92
🟠 Best practices 78
🟠 SEO 82

📱 Mobile

We ran Lighthouse against the changes on a mobile and produced this report. Here's the summary:

Category Score
🟠 Performance 87
🟢 Accessibility 92
🟠 Best practices 78
🟠 SEO 85

@chanceaclark chanceaclark changed the title feat(payment): PAYPAL-5114 POC headless wallet buttons implementation feat(payment): POC headless wallet buttons implementation Apr 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants