-
Notifications
You must be signed in to change notification settings - Fork 221
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
base: master
Are you sure you want to change the base?
PoC HWB #2855
Conversation
address: BillingAddressUpdateRequestBody, | ||
options?: GraphQLRequestOptions, | ||
): Promise<Response<BillingAddress>> { | ||
const path = 'wallet/graphql'; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
🎉 Snyk checks have passed. No issues have been found so far.✅ code/snyk check is complete. No issues have been found. (View Details) |
4a17514
to
71f335c
Compare
There was a problem hiding this 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, |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
What?
PoC related to Headless Wallet Button implementation
Catalyst: bigcommerce/catalyst#2298
Why?
...
Testing / Proof
...
@bigcommerce/team-checkout @bigcommerce/team-payments