@paypal/checkout-components 5.0.307 → 5.0.308

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/globals.js CHANGED
@@ -32,6 +32,7 @@ module.exports = {
32
32
  __CARD_FIELD__: "/smart/card-field",
33
33
  __WALLET__: "/smart/wallet",
34
34
  __PAYMENT_FIELDS__: "/altpayfields",
35
+ __MESSAGE_MODAL__: "https://www.paypalobjects.com/upstream/bizcomponents/js/modal.js",
35
36
  },
36
37
  },
37
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.307",
3
+ "version": "5.0.308",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -114,7 +114,7 @@
114
114
  "@paypal/connect-loader-component": "1.1.1",
115
115
  "@paypal/funding-components": "^1.0.31",
116
116
  "@paypal/sdk-client": "^4.0.184",
117
- "@paypal/sdk-constants": "^1.0.141",
117
+ "@paypal/sdk-constants": "^1.0.146",
118
118
  "@paypal/sdk-logos": "^2.2.6"
119
119
  },
120
120
  "lint-staged": {
@@ -63,3 +63,24 @@ export const MENU_PLACEMENT = {
63
63
  ABOVE: ("above": "above"),
64
64
  BELOW: ("below": "below"),
65
65
  };
66
+
67
+ export const MESSAGE_OFFER = {
68
+ PAY_LATER_LONG_TERM: ("pay_later_long_term": "pay_later_long_term"),
69
+ PAY_LATER_SHORT_TERM: ("pay_later_short_term": "pay_later_short_term"),
70
+ };
71
+
72
+ export const MESSAGE_COLOR = {
73
+ BLACK: ("black": "black"),
74
+ WHITE: ("white": "white"),
75
+ };
76
+
77
+ export const MESSAGE_POSITION = {
78
+ TOP: ("top": "top"),
79
+ BOTTOM: ("bottom": "bottom"),
80
+ };
81
+
82
+ export const MESSAGE_ALIGN = {
83
+ CENTER: ("center": "center"),
84
+ LEFT: ("left": "left"),
85
+ RIGHT: ("right": "right"),
86
+ };
@@ -42,4 +42,9 @@ export const CLASS = {
42
42
  HIDDEN: ("hidden": "hidden"),
43
43
 
44
44
  IMMEDIATE: ("immediate": "immediate"),
45
+
46
+ BUTTON_MESSAGE: ("paypal-button-message": "paypal-button-message"),
47
+
48
+ BUTTON_MESSAGE_RESERVE:
49
+ ("paypal-button-message-reserved": "paypal-button-message-reserved"),
45
50
  };
@@ -18,6 +18,7 @@ declare var __PAYPAL_CHECKOUT__: {|
18
18
  __VENMO__: string,
19
19
  __WALLET__: string,
20
20
  __PAYMENT_FIELDS__: string,
21
+ __MESSAGE_MODAL__: string,
21
22
  |},
22
23
  |};
23
24
 
@@ -214,7 +214,12 @@ export const buildHostedButtonOnApprove = ({
214
214
  // so we need to redirect to the thank you page for buyers who complete
215
215
  // a checkout via "Debit or Credit Card".
216
216
  if (data.paymentSource === FUNDING.CARD) {
217
- window.location = `${baseUrl}/ncp/payment/${hostedButtonId}/${data.orderID}`;
217
+ let redirectUrl = `${baseUrl}/ncp/payment/${hostedButtonId}/${data.orderID}`;
218
+ // add error messages to the payment confirmation page url
219
+ if (response.body?.details?.[0]?.issue) {
220
+ redirectUrl += `?status=${response.body.details[0].issue}`;
221
+ }
222
+ window.location = redirectUrl;
218
223
  }
219
224
  return response;
220
225
  });
@@ -285,6 +285,27 @@ describe("buildHostedButtonOnApprove", () => {
285
285
  "https://example.com/ncp/payment/B1234567890/EC-1234567890"
286
286
  );
287
287
  });
288
+
289
+ test("redirects with an error message in the status query parameter", async () => {
290
+ // $FlowIssue
291
+ request.mockImplementation(() =>
292
+ // eslint-disable-next-line compat/compat
293
+ Promise.resolve({
294
+ body: {
295
+ details: [
296
+ {
297
+ issue: "DUPLICATE_INVOICE_ID",
298
+ },
299
+ ],
300
+ },
301
+ })
302
+ );
303
+
304
+ await onApprove({ orderID, paymentSource: "card" });
305
+ expect(window.location).toBe(
306
+ "https://example.com/ncp/payment/B1234567890/EC-1234567890?status=DUPLICATE_INVOICE_ID"
307
+ );
308
+ });
288
309
  });
289
310
  });
290
311
 
@@ -15,6 +15,7 @@ import {
15
15
  BUTTON_NUMBER,
16
16
  BUTTON_LAYOUT,
17
17
  BUTTON_FLOW,
18
+ MESSAGE_POSITION,
18
19
  } from "../../constants";
19
20
  import {
20
21
  determineEligibleFunding,
@@ -34,6 +35,8 @@ import { Button } from "./button";
34
35
  import { TagLine } from "./tagline";
35
36
  import { Script } from "./script";
36
37
  import { PoweredByPayPal } from "./poweredBy";
38
+ import { Message } from "./message";
39
+ import { calculateShowPoweredBy } from "./util";
37
40
 
38
41
  type GetWalletInstrumentOptions = {|
39
42
  wallet: ?Wallet,
@@ -177,6 +180,8 @@ export function Buttons(props: ButtonsProps): ElementNode {
177
180
  supportedNativeBrowser,
178
181
  showPayLabel,
179
182
  displayOnly,
183
+ message,
184
+ messageMarkup,
180
185
  } = normalizeButtonProps(props);
181
186
  const { layout, shape, tagline } = style;
182
187
 
@@ -237,6 +242,14 @@ export function Buttons(props: ButtonsProps): ElementNode {
237
242
  return i;
238
243
  };
239
244
 
245
+ const showTagline =
246
+ tagline &&
247
+ layout === BUTTON_LAYOUT.HORIZONTAL &&
248
+ !fundingSource &&
249
+ !message;
250
+
251
+ const showPoweredBy = calculateShowPoweredBy(layout, fundingSources);
252
+
240
253
  return (
241
254
  <div
242
255
  class={[
@@ -256,6 +269,10 @@ export function Buttons(props: ButtonsProps): ElementNode {
256
269
  fundingEligibility={fundingEligibility}
257
270
  />
258
271
 
272
+ {message && message.position === MESSAGE_POSITION.TOP ? (
273
+ <Message markup={messageMarkup} position={message.position} />
274
+ ) : null}
275
+
259
276
  {fundingSources.map((source, i) => (
260
277
  <Button
261
278
  content={content}
@@ -285,7 +302,7 @@ export function Buttons(props: ButtonsProps): ElementNode {
285
302
  />
286
303
  ))}
287
304
 
288
- {tagline && layout === BUTTON_LAYOUT.HORIZONTAL && !fundingSource ? (
305
+ {showTagline ? (
289
306
  <TagLine
290
307
  fundingSource={fundingSources[0]}
291
308
  style={style}
@@ -307,9 +324,10 @@ export function Buttons(props: ButtonsProps): ElementNode {
307
324
  />
308
325
  ) : null}
309
326
 
310
- {layout === BUTTON_LAYOUT.VERTICAL &&
311
- fundingSources.indexOf(FUNDING.CARD) !== -1 ? (
312
- <PoweredByPayPal locale={locale} nonce={nonce} />
327
+ {showPoweredBy ? <PoweredByPayPal locale={locale} nonce={nonce} /> : null}
328
+
329
+ {message && message.position === MESSAGE_POSITION.BOTTOM ? (
330
+ <Message markup={messageMarkup} position={message.position} />
313
331
  ) : null}
314
332
 
315
333
  <Script nonce={nonce} />
@@ -0,0 +1,31 @@
1
+ /* @flow */
2
+ /** @jsx node */
3
+
4
+ import { node, type ChildType } from "@krakenjs/jsx-pragmatic/src";
5
+
6
+ import { CLASS } from "../../constants";
7
+
8
+ const INITIAL_RESERVED_HEIGHT = "36px";
9
+
10
+ type MessageProps = {|
11
+ markup: ?string,
12
+ position: string,
13
+ |};
14
+
15
+ export function Message({ markup, position }: MessageProps): ChildType {
16
+ const messageClassNames = [
17
+ CLASS.BUTTON_MESSAGE,
18
+ `${CLASS.BUTTON_MESSAGE}-${position}`,
19
+ ].join(" ");
20
+
21
+ if (typeof markup !== "string") {
22
+ return (
23
+ <div
24
+ class={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
25
+ style={`height:${INITIAL_RESERVED_HEIGHT}`}
26
+ />
27
+ );
28
+ }
29
+
30
+ return <div class={messageClassNames} innerHTML={markup} />;
31
+ }
@@ -40,11 +40,15 @@ import {
40
40
  BUTTON_SIZE,
41
41
  BUTTON_FLOW,
42
42
  MENU_PLACEMENT,
43
+ MESSAGE_OFFER,
44
+ MESSAGE_COLOR,
45
+ MESSAGE_POSITION,
46
+ MESSAGE_ALIGN,
43
47
  } from "../../constants";
44
48
  import { getFundingConfig, isFundingEligible } from "../../funding";
45
49
 
46
50
  import { BUTTON_SIZE_STYLE } from "./config";
47
- import { isBorderRadiusNumber } from "./util";
51
+ import { isBorderRadiusNumber, calculateMessagePosition } from "./util";
48
52
 
49
53
  export type CreateOrderData = {||} | {||};
50
54
 
@@ -424,6 +428,22 @@ export type ApplePaySessionConfigRequest = (
424
428
  request: Object
425
429
  ) => ApplePaySessionConfig;
426
430
 
431
+ export type ButtonMessage = {|
432
+ amount?: number,
433
+ offer?: $ReadOnlyArray<$Values<typeof MESSAGE_OFFER>>,
434
+ color: $Values<typeof MESSAGE_COLOR>,
435
+ position: $Values<typeof MESSAGE_POSITION>,
436
+ align: $Values<typeof MESSAGE_ALIGN>,
437
+ |};
438
+
439
+ export type ButtonMessageInputs = {|
440
+ amount?: number | void,
441
+ offer?: $ReadOnlyArray<$Values<typeof MESSAGE_OFFER>> | void,
442
+ color?: $Values<typeof MESSAGE_COLOR> | void,
443
+ position?: $Values<typeof MESSAGE_POSITION> | void,
444
+ align?: $Values<typeof MESSAGE_ALIGN> | void,
445
+ |};
446
+
427
447
  export type RenderButtonProps = {|
428
448
  style: ButtonStyle,
429
449
  locale: LocaleType,
@@ -458,6 +478,8 @@ export type RenderButtonProps = {|
458
478
  supportedNativeBrowser: boolean,
459
479
  showPayLabel: boolean,
460
480
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
481
+ message?: ButtonMessage,
482
+ messageMarkup?: string,
461
483
  |};
462
484
 
463
485
  export type PrerenderDetails = {|
@@ -517,6 +539,8 @@ export type ButtonProps = {|
517
539
  createVaultSetupToken: CreateVaultSetupToken,
518
540
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
519
541
  hostedButtonId?: string,
542
+ message?: ButtonMessage,
543
+ messageMarkup?: string,
520
544
  |};
521
545
 
522
546
  // eslint-disable-next-line flowtype/require-exact-type
@@ -559,6 +583,9 @@ export type ButtonPropsInputs = {
559
583
  supportedNativeBrowser: boolean,
560
584
  showPayLabel: boolean,
561
585
  displayOnly: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
586
+ message?: ButtonMessageInputs | void,
587
+ messageMarkup?: string | void,
588
+ renderedButtons: $ReadOnlyArray<$Values<typeof FUNDING>>,
562
589
  };
563
590
 
564
591
  export const DEFAULT_STYLE = {
@@ -707,6 +734,72 @@ export function normalizeButtonStyle(
707
734
  };
708
735
  }
709
736
 
737
+ export function normalizeButtonMessage(
738
+ message: ButtonMessageInputs,
739
+ layout: $Values<typeof BUTTON_LAYOUT>,
740
+ fundingSources: $ReadOnlyArray<$Values<typeof FUNDING>>
741
+ ): ButtonMessage {
742
+ const {
743
+ amount,
744
+ offer,
745
+ color = MESSAGE_COLOR.BLACK,
746
+ position,
747
+ align = MESSAGE_ALIGN.CENTER,
748
+ } = message;
749
+
750
+ if (typeof amount !== "undefined") {
751
+ if (typeof amount !== "number") {
752
+ throw new TypeError(
753
+ `Expected message.amount to be a number, got: ${amount}`
754
+ );
755
+ }
756
+ if (amount < 0) {
757
+ throw new Error(
758
+ `Expected message.amount to be a positive number, got: ${amount}`
759
+ );
760
+ }
761
+ }
762
+
763
+ if (typeof offer !== "undefined") {
764
+ if (!Array.isArray(offer)) {
765
+ throw new TypeError(
766
+ `Expected message.offer to be an array of strings, got: ${String(
767
+ offer
768
+ )}`
769
+ );
770
+ }
771
+ const invalidOffers = offer.filter(
772
+ (o) => !values(MESSAGE_OFFER).includes(o)
773
+ );
774
+ if (invalidOffers.length > 0) {
775
+ throw new Error(`Invalid offer(s): ${invalidOffers.join(",")}`);
776
+ }
777
+ }
778
+
779
+ if (typeof color !== "undefined" && !values(MESSAGE_COLOR).includes(color)) {
780
+ throw new Error(`Invalid color: ${color}`);
781
+ }
782
+
783
+ if (
784
+ typeof position !== "undefined" &&
785
+ !values(MESSAGE_POSITION).includes(position)
786
+ ) {
787
+ throw new Error(`Invalid position: ${position}`);
788
+ }
789
+
790
+ if (typeof align !== "undefined" && !values(MESSAGE_ALIGN).includes(align)) {
791
+ throw new Error(`Invalid align: ${align}`);
792
+ }
793
+
794
+ return {
795
+ amount,
796
+ offer,
797
+ color,
798
+ position: calculateMessagePosition(fundingSources, layout, position),
799
+ align,
800
+ };
801
+ }
802
+
710
803
  const COUNTRIES = values(COUNTRY);
711
804
  const FUNDING_SOURCES = values(FUNDING);
712
805
  const ENVS = values(ENV);
@@ -761,6 +854,9 @@ export function normalizeButtonProps(
761
854
  supportedNativeBrowser = false,
762
855
  showPayLabel = true,
763
856
  displayOnly = [],
857
+ message,
858
+ messageMarkup,
859
+ renderedButtons,
764
860
  } = props;
765
861
 
766
862
  const { country, lang } = locale;
@@ -819,6 +915,11 @@ export function normalizeButtonProps(
819
915
  }
820
916
 
821
917
  style = normalizeButtonStyle(props, style);
918
+ const { layout } = style;
919
+
920
+ message = message
921
+ ? normalizeButtonMessage(message, layout, renderedButtons)
922
+ : undefined;
822
923
 
823
924
  return {
824
925
  clientID,
@@ -852,5 +953,7 @@ export function normalizeButtonProps(
852
953
  supportedNativeBrowser,
853
954
  showPayLabel,
854
955
  displayOnly,
956
+ message,
957
+ messageMarkup,
855
958
  };
856
959
  }
@@ -1,5 +1,41 @@
1
1
  /* @flow */
2
+ import { FUNDING } from "@paypal/sdk-constants/src";
3
+
4
+ import { BUTTON_LAYOUT, MESSAGE_POSITION } from "../../constants";
5
+ import { ValidationError } from "../../lib";
2
6
 
3
7
  export function isBorderRadiusNumber(borderRadius?: number): boolean {
4
8
  return typeof borderRadius === "number";
5
9
  }
10
+
11
+ export function calculateShowPoweredBy(
12
+ layout: $Values<typeof BUTTON_LAYOUT>,
13
+ fundingSources: $ReadOnlyArray<$Values<typeof FUNDING>>
14
+ ): boolean {
15
+ return (
16
+ layout === BUTTON_LAYOUT.VERTICAL && fundingSources.includes(FUNDING.CARD)
17
+ );
18
+ }
19
+
20
+ export function calculateMessagePosition(
21
+ fundingSources: $ReadOnlyArray<$Values<typeof FUNDING>>,
22
+ layout: $Values<typeof BUTTON_LAYOUT>,
23
+ position?: $Values<typeof MESSAGE_POSITION>
24
+ ): $Values<typeof MESSAGE_POSITION> {
25
+ const showPoweredBy = calculateShowPoweredBy(layout, fundingSources);
26
+
27
+ if (showPoweredBy && position === MESSAGE_POSITION.BOTTOM) {
28
+ throw new ValidationError(
29
+ "Message position must be 'top' when Debit and/or Credit Card button is present"
30
+ );
31
+ }
32
+
33
+ if (
34
+ showPoweredBy ||
35
+ position === MESSAGE_POSITION.TOP ||
36
+ (layout === BUTTON_LAYOUT.VERTICAL && !position)
37
+ ) {
38
+ return MESSAGE_POSITION.TOP;
39
+ }
40
+ return MESSAGE_POSITION.BOTTOM;
41
+ }
@@ -73,7 +73,11 @@ import {
73
73
  logLatencyInstrumentationPhase,
74
74
  prepareInstrumentationPayload,
75
75
  } from "../../lib";
76
- import { normalizeButtonStyle, type ButtonProps } from "../../ui/buttons/props";
76
+ import {
77
+ normalizeButtonStyle,
78
+ normalizeButtonMessage,
79
+ type ButtonProps,
80
+ } from "../../ui/buttons/props";
77
81
  import { isFundingEligible } from "../../funding";
78
82
 
79
83
  import { containerTemplate } from "./container";
@@ -86,6 +90,7 @@ import {
86
90
  getRenderedButtons,
87
91
  getButtonSize,
88
92
  getButtonExperiments,
93
+ getModal,
89
94
  } from "./util";
90
95
 
91
96
  export type ButtonsComponent = ZoidComponent<ButtonProps>;
@@ -635,6 +640,24 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
635
640
  value: getMerchantRequestedPopupsDisabled,
636
641
  },
637
642
 
643
+ message: {
644
+ type: "object",
645
+ queryParam: true,
646
+ required: false,
647
+ decorate: ({ props, value }) => {
648
+ const {
649
+ style: { layout },
650
+ renderedButtons: fundingSources,
651
+ } = props;
652
+ return normalizeButtonMessage(
653
+ // $FlowFixMe
654
+ value,
655
+ layout,
656
+ fundingSources
657
+ );
658
+ },
659
+ },
660
+
638
661
  nonce: {
639
662
  type: "string",
640
663
  default: getCSPNonce,
@@ -684,6 +707,103 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
684
707
  },
685
708
  },
686
709
 
710
+ onMessageClick: {
711
+ type: "function",
712
+ required: false,
713
+ value: ({ props }) => {
714
+ return async ({
715
+ offerType,
716
+ messageType,
717
+ offerCountryCode,
718
+ creditProductIdentifier,
719
+ }) => {
720
+ const { message, clientID, merchantID, currency, buttonSessionID } =
721
+ props;
722
+ const amount = message?.amount;
723
+
724
+ getLogger()
725
+ .info("button_message_click")
726
+ .track({
727
+ [FPTI_KEY.TRANSITION]: "button_message_click",
728
+ [FPTI_KEY.STATE]: "BUTTON_MESSAGE",
729
+ [FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
730
+ [FPTI_KEY.CONTEXT_ID]: buttonSessionID,
731
+ [FPTI_KEY.CONTEXT_TYPE]: "button_session_id",
732
+ [FPTI_KEY.EVENT_NAME]: "message_click",
733
+ [FPTI_KEY.SELLER_ID]: merchantID?.join(","),
734
+ [FPTI_KEY.BUTTON_MESSAGE_OFFER_TYPE]: offerType,
735
+ [FPTI_KEY.BUTTON_MESSAGE_CREDIT_PRODUCT_IDENTIFIER]:
736
+ creditProductIdentifier,
737
+ [FPTI_KEY.BUTTON_MESSAGE_TYPE]: messageType,
738
+ [FPTI_KEY.BUTTON_MESSAGE_POSITION]: message?.position,
739
+ [FPTI_KEY.BUTTON_MESSAGE_ALIGN]: message?.align,
740
+ [FPTI_KEY.BUTTON_MESSAGE_COLOR]: message?.color,
741
+ [FPTI_KEY.BUTTON_MESSAGE_OFFER_COUNTRY]: offerCountryCode,
742
+ [FPTI_KEY.BUTTON_MESSAGE_CURRENCY]: currency,
743
+ [FPTI_KEY.BUTTON_MESSAGE_AMOUNT]: amount,
744
+ });
745
+
746
+ const modalInstance = await getModal(clientID, merchantID);
747
+ return modalInstance?.show({
748
+ amount,
749
+ offer: offerType,
750
+ currency,
751
+ });
752
+ };
753
+ },
754
+ },
755
+
756
+ onMessageHover: {
757
+ type: "function",
758
+ required: false,
759
+ value: ({ props }) => {
760
+ return () => {
761
+ // offerType, messageType, offerCountryCode, and creditProductIdentifier are passed in and may be used in an upcoming message hover logging feature
762
+
763
+ // lazy loads the modal, to be memoized and executed onMessageClick
764
+ const { clientID, merchantID } = props;
765
+ return getModal(clientID, merchantID);
766
+ };
767
+ },
768
+ },
769
+
770
+ onMessageReady: {
771
+ type: "function",
772
+ required: false,
773
+ value: ({ props }) => {
774
+ return ({
775
+ offerType,
776
+ messageType,
777
+ offerCountryCode,
778
+ creditProductIdentifier,
779
+ }) => {
780
+ const { message, buttonSessionID, currency, merchantID } = props;
781
+
782
+ getLogger()
783
+ .info("button_message_render")
784
+ .track({
785
+ [FPTI_KEY.TRANSITION]: "button_message_render",
786
+ [FPTI_KEY.STATE]: "BUTTON_MESSAGE",
787
+ [FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
788
+ [FPTI_KEY.CONTEXT_ID]: buttonSessionID,
789
+ [FPTI_KEY.CONTEXT_TYPE]: "button_session_id",
790
+ [FPTI_KEY.EVENT_NAME]: "message_render",
791
+ [FPTI_KEY.SELLER_ID]: merchantID?.join(","),
792
+ [FPTI_KEY.BUTTON_MESSAGE_OFFER_TYPE]: offerType,
793
+ [FPTI_KEY.BUTTON_MESSAGE_CREDIT_PRODUCT_IDENTIFIER]:
794
+ creditProductIdentifier,
795
+ [FPTI_KEY.BUTTON_MESSAGE_TYPE]: messageType,
796
+ [FPTI_KEY.BUTTON_MESSAGE_POSITION]: message?.position,
797
+ [FPTI_KEY.BUTTON_MESSAGE_ALIGN]: message?.align,
798
+ [FPTI_KEY.BUTTON_MESSAGE_COLOR]: message?.color,
799
+ [FPTI_KEY.BUTTON_MESSAGE_CURRENCY]: currency,
800
+ [FPTI_KEY.BUTTON_MESSAGE_OFFER_COUNTRY]: offerCountryCode,
801
+ [FPTI_KEY.BUTTON_MESSAGE_AMOUNT]: message?.amount,
802
+ });
803
+ };
804
+ },
805
+ },
806
+
687
807
  onShippingAddressChange: {
688
808
  type: "function",
689
809
  required: false,
@@ -13,6 +13,7 @@ import {
13
13
  getElement,
14
14
  isStandAlone,
15
15
  once,
16
+ memoize,
16
17
  } from "@krakenjs/belter/src";
17
18
  import { FUNDING } from "@paypal/sdk-constants/src";
18
19
  import {
@@ -22,6 +23,8 @@ import {
22
23
  getFundingEligibility,
23
24
  getPlatform,
24
25
  getComponents,
26
+ getEnv,
27
+ getNamespace,
25
28
  } from "@paypal/sdk-client/src";
26
29
  import { getRefinedFundingEligibility } from "@paypal/funding-components/src";
27
30
 
@@ -357,3 +360,53 @@ export function getButtonSize(
357
360
  }
358
361
  }
359
362
  }
363
+
364
+ function buildModalBundleUrl(): string {
365
+ let url = __PAYPAL_CHECKOUT__.__URI__.__MESSAGE_MODAL__;
366
+ if (getEnv() === "sandbox") {
367
+ url = url.replace("/js/", "/sandbox/");
368
+ } else if (getEnv() === "stage" || getEnv() === "local") {
369
+ url = url.replace("/js/", "/stage/");
370
+ }
371
+ return url;
372
+ }
373
+
374
+ export const getModal: (
375
+ clientID: string,
376
+ merchantID: $ReadOnlyArray<string> | void
377
+ ) => Object = memoize(async (clientID, merchantID) => {
378
+ try {
379
+ const namespace = getNamespace();
380
+ if (!window[namespace].MessagesModal) {
381
+ // eslint-disable-next-line no-restricted-globals, promise/no-native
382
+ await new Promise((resolve, reject) => {
383
+ const script = document.createElement("script");
384
+ script.setAttribute("data-pp-namespace", namespace);
385
+ script.src = buildModalBundleUrl();
386
+ script.addEventListener("error", (err: Event) => {
387
+ reject(err);
388
+ });
389
+ script.addEventListener("load", () => {
390
+ document.body?.removeChild(script);
391
+ resolve();
392
+ });
393
+ document.body?.appendChild(script);
394
+ });
395
+ }
396
+
397
+ return window[namespace].MessagesModal({
398
+ account: `client-id:${clientID}`,
399
+ merchantId: merchantID?.join(",") || undefined,
400
+ });
401
+ } catch (err) {
402
+ // $FlowFixMe flow doesn't seem to understand that the reset function property exists on the function object itself
403
+ getModal.reset();
404
+ getLogger()
405
+ .error("button_message_modal_fetch_error", { err })
406
+ .track({
407
+ err: err.message || "BUTTON_MESSAGE_MODAL_FETCH_ERROR",
408
+ details: err.details,
409
+ stack: JSON.stringify(err.stack || err),
410
+ });
411
+ }
412
+ });