Hi Stackoverflow community,
I need help in loading Paypal messages in web components. After loading the paypal SDK, I am trying to include the Paypal pay later messages with the code below.
window.paypal.Messages({
amount: this.amount,
placement: "product",
style: {
layout: "text",
logo: {
type: "inline",
},
},
}).render(this.shadowRoot!.querySelector("#paypal-message") as HTMLElement);
I am getting the following error in the browser console.
paypal_messages_not_in_document
description: "Container must be in the document."
timestamp: "1651659388515"
I am able to load the paypal buttons with the same logic.
window.paypal.Buttons({ ....... }).render(this.shadowRoot!.querySelector("#paypal-button") as HTMLElement);
Below is the web component in lit element framework.
import { customElement, html, internalProperty, property } from "lit-element";
import { RmsBookstoreAddress, PaymentDetails, PaymentType } from "../../../../features/shop-checkout";
import { BaseComponent } from "../../../../services/base-component";
import { booleanConverter } from "../../../../services/converters";
import { waitForElementsToLoad } from "../../../../services/delayUtil";
import emit from "../../../../services/events";
/* WARNING: Do NOT import directly from "braintree-web", it causes the bundle size to increase dramatically. */
import client from "braintree-web/client";
import dataCollector from "braintree-web/data-collector";
import paypalCheckout, { PayPalCheckoutTokenizationOptions } from "braintree-web/paypal-checkout";
/**
* Configure and load Paypal button.
* Saves paypal payment details in redux.
*/
@customElement("paypal-button")
export default class PaypalButton extends BaseComponent {
/**
* Flag to indicate the checkout contains shippable items.
*/
@property({ converter: booleanConverter })
shippable = false;
/**
* Braintree Client token to initialize Paypal button.
*/
@property()
clientToken = "";
/**
* Currency for Paypal transaction.
*/
@property()
currency = "USD";
/**
* Option to set the description of the preapproved payment agreement visible to customers in their PayPal profile during Vault flows. Max 255 characters.
*/
@property()
billingAgreementDescription = "";
/**
* Transaction amount to be displayed in Paypal.
*/
@property({type: Number })
amount = 0;
/**
* Value to override paypal shipping address.
*/
@property({attribute: false, type: Object})
shippingAddress;
/**
* Allow PayPal to Capture the shipping address.
*/
@property({ converter: booleanConverter })
usePayPalShippingAddress = false;
@property({ converter: booleanConverter})
userAutoLogin = false;
/**
* Billing address returned by Paypal
*/
@internalProperty()
private internalBillingAddress: RmsBookstoreAddress | undefined;
/**
* Shipping address returned by Paypal
*/
@internalProperty()
private internalShippingAddress: RmsBookstoreAddress | undefined;
/**
* Paypal payment details
*/
@internalProperty()
private paymentDetails: PaymentDetails | undefined;
renderComp() {
return html`
<div id="paypal-button"></div>
<div id="paypal-message"></div>
`;
}
/**
* Wait for the paypal button place order to render before adding the paypal button to it.
* @param _changedProperties
*/
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
await waitForElementsToLoad(this.shadowRoot!, [
"#paypal-button",
]);
this.setupPaypalButton();
}
setupPaypalButton(){
//create braintree client instance
client.create({
authorization: this.clientToken
}).then( clientInstance =>{
//collect device data
dataCollector.create({
client: clientInstance,
}).then((dataCollectorInstance)=>{
const paypalPaymentDeviceData = dataCollectorInstance.deviceData;
//paypal button shipping config
let shippingConfig = {};
let intent: "capture" | "authorize" = "capture" // for digital products intent is capture
if(this.shippable){
intent = 'authorize'; // for physical or mixed cart products intent is authorize
if(!this.usePayPalShippingAddress && this.shippingAddress){
shippingConfig = {
enableShippingAddress: true,
shippingAddressEditable: false,
shippingAddressOverride: {
recipientName: `${this.shippingAddress.firstName} ${this.shippingAddress.lastName}`,
line1: `${this.shippingAddress.address1}`,
line2: `${this.shippingAddress.address2 ? this.shippingAddress.address2 : ''}`,
city: `${this.shippingAddress.city}`,
countryCode: `${this.shippingAddress.country}`,
postalCode: `${this.shippingAddress.zipCode}`,
state: `${this.shippingAddress.state}`,
phone: `${this.shippingAddress.phoneNumber}`
}
}
} else if (this.usePayPalShippingAddress) {
shippingConfig = {
enableShippingAddress: true,
shippingAddressEditable: true
}
}
}
//create paypal button
paypalCheckout.create({
client: clientInstance,
autoSetDataUserIdToken: this.userAutoLogin
}).then( paypalCheckoutInstance => {
paypalCheckoutInstance.loadPayPalSDK({
components: 'buttons,messages',
currency: this.currency,
intent: intent,
}).then( () => {
window.paypal.Messages({
amount: this.amount,
placement: "product",
style: {
layout: "text",
logo: {
type: "inline",
},
},
}).render(this.shadowRoot!.querySelector("#paypal-message") as HTMLElement);
window.paypal.Buttons({
fundingSource: window.paypal.FUNDING.PAYPAL,
createOrder: () => {
return paypalCheckoutInstance.createPayment({
flow: 'checkout',
amount: this.amount,
currency: this.currency,
requestBillingAgreement: true,
billingAgreementDescription: this.billingAgreementDescription,
intent: intent,
...shippingConfig,
});
},
onApprove: (_data: PayPalCheckoutTokenizationOptions, _actions: any) => {
return paypalCheckoutInstance.tokenizePayment(_data).then( payload => {
const paypalBillingAddress: any = payload.details.billingAddress;
this.internalBillingAddress = {
firstName: payload.details.firstName,
lastName: payload.details.lastName,
phoneNumber: payload.details.phone ? payload.details.phone : '',
country: paypalBillingAddress.countryCode,
address1: paypalBillingAddress.line1,
address2: paypalBillingAddress.line2,
city: paypalBillingAddress.city,
state: paypalBillingAddress.state,
zipCode: paypalBillingAddress.postalCode,
};
this.paymentDetails = {
paymentType: PaymentType.BRAINTREE_PAYPAL,
paymentNonce: payload.nonce,
deviceData: paypalPaymentDeviceData,
paypalEmail: payload.details.email
}
if (this.usePayPalShippingAddress) {
const paypalShippingAddress: any = payload.details.shippingAddress;
this.internalShippingAddress = {
firstName: payload.details.firstName,
lastName: payload.details.lastName,
phoneNumber: payload.details.phone ? payload.details.phone : '',
country: paypalShippingAddress.countryCode,
address1: paypalShippingAddress.line1,
address2: paypalShippingAddress.line2,
city: paypalShippingAddress.city,
state: paypalShippingAddress.state,
zipCode: paypalShippingAddress.postalCode,
};
}
emit({
element: this,
name: `mhecomm-paypal-checkout-info-collected`,
detail: {
billingAddress: this.internalBillingAddress,
paymentDetails: this.paymentDetails,
shippingAddress: this.internalShippingAddress,
},
});
});
},
onCancel: (data: any) => {
console.log('PayPal payment cancelled', JSON.stringify(data));
},
onError: (err: any) => {
console.error('PayPal error', err);
}
}).render(this.shadowRoot!.querySelector("#paypal-button") as HTMLElement);
//console.log(paypalObj)
//console.log(this.shadowRoot!.querySelector("#paypal-message") as HTMLElement)
});
});
});
});
}
}
declare global {
interface HTMLElementTagNameMap {
'paypal-button': PaypalButton;
}
}
How is 'renderComp' called? Not sure if that's a standard callback for your framework, but based on the error it would appear the that the
<div>
for displaying the messages does not exist in the DOM at the time the messages are being rendered:You should work to ensure the
<div>
with that id exists. If necessary, you can add some logging above the window.paypal.Messages invocation to verify the container with that id exists at the moment that JS is being run.Also the problem could be related to the use of a Shadow DOM; ensure that the #paypal-message container can be found in the main page's DOM which is where window.paypal.Messages will look for it.