Skip to content

PoC HWB #2855

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 1 commit into
base: master
Choose a base branch
from
Draft

PoC HWB #2855

wants to merge 1 commit into from

Conversation

bc-nick
Copy link
Contributor

@bc-nick bc-nick commented May 9, 2025

What?

PoC related to Headless Wallet Button implementation

Catalyst: bigcommerce/catalyst#2298

Why?

...

Testing / Proof

...

@bigcommerce/team-checkout @bigcommerce/team-payments

@bc-nick bc-nick changed the title POC HWB PoC HWB May 9, 2025
address: BillingAddressUpdateRequestBody,
options?: GraphQLRequestOptions,
): Promise<Response<BillingAddress>> {
const path = 'wallet/graphql';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This endpoint will be changed to the appropriate one

};
}

export default class HeadlessWalletButtonIntegrationRequestService {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class will be included all required graphql requests for wallet button initialization process, such as: updateBillingAddress/addBillingAddress, updateShippingAddress/addSippingAddress, updateShippingOption, getShippingCoutryList

Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest splitting the class into smaller ones, otherwise it'd become very big if we add all the methods. Perhaps you can consider the following grouping? BillingRequestSender, ShippingRequestSender, PaymentRequestSender etc...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, sure I am working on it. Thanks

@@ -0,0 +1,23 @@
{
"root": "packages/headless-wallet-button-integration",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Service will be renamed to wallet-button-integration

createCartRedirectUrls(
input: {
paymentWalletData: {
initializationId: 51,
Copy link
Contributor Author

@bc-nick bc-nick May 12, 2025

Choose a reason for hiding this comment

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

As far as I understood initializationId will be taken from session on Catalyst side using middleware. initializationId was set using createPaymentOrderIntent. But in this case we have to modify graphql query while querying that query. @davidchin is it ok to modify data in this way?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what you meant by this. But consider using GQL variables (e.g.: $input) instead of directly interpolating the input values in the mutation query. Otherwise, it could lead to potential injection attacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@koa75 can I ask you to provide information about how it have to work to be sure that everything is aligned?

Copy link
Contributor

@koa75 koa75 May 15, 2025

Choose a reason for hiding this comment

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

Hi @davidchin We discussed this case in a private chat. When we create a payment intent, we should not return the initializationId to the client part, but save it in the session. This data is needed so that all requests that pass InitializationId take them on the server side (not the client side) and add them to the necessary mutations (for example, createCartRedirectUrls). Therefore, I had a concern in using graphQL proxy. This was easy to do using the rest API proxy

Copy link
Contributor

@koa75 koa75 May 15, 2025

Choose a reason for hiding this comment

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

Maybe the security team will allow us to use initializationId on the client side, since PayPal checks which merchant is sending this request. We discussed this issue in PR https://github.com/bigcommerce/storefront/pull/3675#discussion_r2007069833

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @koa75. I actually didn't know what initializationId was. My original was referring to the fact that the query was constructed with a hardcoded value instead of a variable, not whether the value could be exposed to the browser.

For our SF REST API, the value is stored against BC session. However, since SF GQL needs to work without BC session, we need somewhere to store the value once it's generated for a checkout session. Rather than exposing the value to Catalyst via our public API, and having to propagate the value back to BC during the checkout redirect, could we just store the token temporarily against a cart ID in the backend (e.g.: Redis). Then, when we need the value for payment processing, we could just retrieve it in the backend by cart ID without needing the client to pass anything?

Copy link
Contributor

Choose a reason for hiding this comment

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

I rather the value not be hardcoded and if it was hardcoded would we really need to display it client side. Also would different initialisation id do different actions it was modified (unsure if there is certain values does certain actions).

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually didn't know what initializationId was

InitializationId is needed on the Catalyst side only for the purpose of executing a redirect to the checkout page. When generating the URL for this redirect, a token is added with all the parameters that should be added to the checkout session. InitializationId is a unique identifier by which the transactionId can be retrieved in order to obtain the necessary information from the provider side (for example, shipping address) on the checkout page.

Rather than exposing the value to Catalyst via our public API, and having to propagate the value back to BC during the checkout redirect, could we just store the token temporarily against a cart ID in the backend (e.g.: Redis)

Yes, this can be done on the server side of Catalyst using a session or Redis. Since the GQL API should be stateless, we should rely only on the data that is in the request, and not on the data that is in the cache. Just from the server side, we can transfer any sensitive data without a state.

Copy link
Contributor

Choose a reason for hiding this comment

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

@koa75 I wasn't referring to Catalyst session. What I meant is that there's no need to return initializationId to Catalyst at all, since Catalyst doesn't actually use it. It's only returned so that Catalyst can pass the ID back to BC during the redirect. If the createPaymentWalletIntent mutation links the initializationId with the cartId on BC side, then we should be able to retrieve the initializationId using the cartId upon redirect, eliminating the need to pass it from Catalyst.

Copy link
Contributor

@koa75 koa75 May 22, 2025

Choose a reason for hiding this comment

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

If the createPaymentWalletIntent mutation links the initializationId with the cartId on BC side, then we should be able to retrieve the initializationId using the cartId upon redirect, eliminating the need to pass it from Catalyst.

@davidchin Yes, I completely agree with you. But now initializationId is stored in the session, so to fix this we need to spend a lot of time on spike and refactoring to move initializationId from session to cache. We will try to fix this.

import HeadlessCheckoutWalletInitializer from './headless-checkout-wallet-initializer';
import HeadlessCheckoutWalletStrategyActionCreator from './headless-checkout-wallet-strategy-action-creator';

export default function createHeadlessCheckoutWalletInitializer(): HeadlessCheckoutWalletInitializer {
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend not scoping this under "headless" for two reasons:

  • Once we have native hosting, Catalyst can be either headless or native.
  • We want to minimise split implementation. Ideally, this new service should work across Catalyst and Stencil.

return this._state;
}

subscribe(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm unsure why we need subscribe or getState for the wallet buttons. I recommend removing these methods and keeping the interface as minimal as possible unless there's a need to expose them. Otherwise, we'll be responsible for maintaining backward compatibility, so we should avoid exposing unnecessary methods that could increase long-term maintenance costs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@davidchin the reason why I decided to add it is to give an opportunity retrieve relevant information about current initialisation statuses of available wallet buttons. It can be useful when we want to make some actions based on these statuses. Maybe would be better to leave it as it is?


initializeHeadlessButton(
options: CheckoutButtonInitializeOptions,
): Promise<CheckoutButtonSelectors> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise, return Promise<void> if you don't really need to return anything besides letting the caller know that initialisation is complete.

RequestOptions,
} from '@bigcommerce/checkout-sdk/payment-integration-api';

export default interface HeadlessWalletButtonIntegrationService {
Copy link
Contributor

Choose a reason for hiding this comment

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

I can see you're trying to mimic the structure of PaymentIntegrationService. I believe we can simplify the structure by not having an interface and a separate class implementing the interface. The reason we have extra layering for PaymentIntegrationService is to establish a module boundary between core and various payment integrations, which are owned by different teams. Since everything related to wallet buttons is being implemented by the PI team for the wallet buttons, I don't see why there's a need for this additional abstraction, at least for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@davidchin I have updated this implementation according to your comments. Take a look pls

};
}

export default class HeadlessWalletButtonIntegrationRequestService {
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest splitting the class into smaller ones, otherwise it'd become very big if we add all the methods. Perhaps you can consider the following grouping? BillingRequestSender, ShippingRequestSender, PaymentRequestSender etc...

import { RequestSender, Response } from '@bigcommerce/request-sender';

import {
BillingAddress,
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe these types come from REST API interface, which may differ from GraphQL, therefore we can't reuse the types. I suggest defining separate ones for GraphQL. Instead of manually defining them, consider using tools such as graphql-codegen that can automatically generate the types based on a GraphQL schema.

createCartRedirectUrls(
input: {
paymentWalletData: {
initializationId: 51,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what you meant by this. But consider using GQL variables (e.g.: $input) instead of directly interpolating the input values in the mutation query. Otherwise, it could lead to potential injection attacks.

@bcsnyk
Copy link
Contributor

bcsnyk commented May 20, 2025

🎉 Snyk checks have passed. No issues have been found so far.

code/snyk check is complete. No issues have been found. (View Details)

@bc-nick bc-nick force-pushed the PAYPAL-5461 branch 4 times, most recently from 4a17514 to 71f335c Compare May 22, 2025 13:39
Copy link
Contributor

@davidchin davidchin left a comment

Choose a reason for hiding this comment

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

Nice work @bc-nick! Thank your for patience. I don't have anything to add besides these small comments. I think you're good to break up this POC PR into smaller ones and write tests for each.

constructor(private readonly requestSender: RequestSender) {}

async updateBillingAddress(
graphQLEndpoint: string,
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be passed as a constructor parameter, since it doesn't change after initialisation. The same pattern can be applied to other request senders.

const document = `
mutation CheckoutRedirectMutation($input: CreateCartRedirectUrlsInput!) {
cart {
createCartRedirectUrls(input: $input) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This mutation doesn't belong to the payment namespace as it is used to generate a cart redirect URL.

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.

6 participants