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;
}
}