@paypal/checkout-components 5.0.297 → 5.0.298-alpha.3

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.
@@ -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,
@@ -36,6 +37,8 @@ import { Button } from "./button";
36
37
  import { TagLine } from "./tagline";
37
38
  import { Script } from "./script";
38
39
  import { PoweredByPayPal } from "./poweredBy";
40
+ import { Message } from "./message";
41
+ import { calculateMessagePosition } from "./util";
39
42
 
40
43
  type GetWalletInstrumentOptions = {|
41
44
  wallet: ?Wallet,
@@ -179,6 +182,8 @@ export function Buttons(props: ButtonsProps): ElementNode {
179
182
  supportedNativeBrowser,
180
183
  showPayLabel,
181
184
  displayOnly,
185
+ message,
186
+ messageMarkup,
182
187
  } = normalizeButtonProps(props);
183
188
  const { layout, shape, tagline } = style;
184
189
 
@@ -240,6 +245,20 @@ export function Buttons(props: ButtonsProps): ElementNode {
240
245
  return i;
241
246
  };
242
247
 
248
+ const showTagline =
249
+ tagline &&
250
+ layout === BUTTON_LAYOUT.HORIZONTAL &&
251
+ !fundingSource &&
252
+ !message;
253
+ const showPoweredBy =
254
+ layout === BUTTON_LAYOUT.VERTICAL && fundingSources.includes(FUNDING.CARD);
255
+
256
+ const calculatedMessagePosition = calculateMessagePosition({
257
+ message,
258
+ showPoweredBy,
259
+ layout,
260
+ });
261
+
243
262
  return (
244
263
  <div
245
264
  class={[
@@ -259,6 +278,10 @@ export function Buttons(props: ButtonsProps): ElementNode {
259
278
  fundingEligibility={fundingEligibility}
260
279
  />
261
280
 
281
+ {message && calculatedMessagePosition === MESSAGE_POSITION.TOP ? (
282
+ <Message markup={messageMarkup} position={calculatedMessagePosition} />
283
+ ) : null}
284
+
262
285
  {fundingSources.map((source, i) => (
263
286
  <Button
264
287
  content={content}
@@ -288,7 +311,7 @@ export function Buttons(props: ButtonsProps): ElementNode {
288
311
  />
289
312
  ))}
290
313
 
291
- {tagline && layout === BUTTON_LAYOUT.HORIZONTAL && !fundingSource ? (
314
+ {showTagline ? (
292
315
  <TagLine
293
316
  fundingSource={fundingSources[0]}
294
317
  style={style}
@@ -310,9 +333,10 @@ export function Buttons(props: ButtonsProps): ElementNode {
310
333
  />
311
334
  ) : null}
312
335
 
313
- {layout === BUTTON_LAYOUT.VERTICAL &&
314
- fundingSources.indexOf(FUNDING.CARD) !== -1 ? (
315
- <PoweredByPayPal locale={locale} nonce={nonce} />
336
+ {showPoweredBy ? <PoweredByPayPal locale={locale} nonce={nonce} /> : null}
337
+
338
+ {message && calculatedMessagePosition === MESSAGE_POSITION.BOTTOM ? (
339
+ <Message markup={messageMarkup} position={calculatedMessagePosition} />
316
340
  ) : null}
317
341
 
318
342
  {buttonDesignScript ? (
@@ -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
+ className={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
25
+ style={`height:${INITIAL_RESERVED_HEIGHT}`}
26
+ />
27
+ );
28
+ }
29
+
30
+ return <div class={messageClassNames} innerHTML={markup} />;
31
+ }
@@ -40,6 +40,10 @@ 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
 
@@ -432,6 +436,22 @@ export type ApplePaySessionConfigRequest = (
432
436
  request: Object
433
437
  ) => ApplePaySessionConfig;
434
438
 
439
+ export type ButtonMessage = {|
440
+ amount?: number,
441
+ offer?: $ReadOnlyArray<$Values<typeof MESSAGE_OFFER>>,
442
+ color?: $Values<typeof MESSAGE_COLOR>,
443
+ position?: $Values<typeof MESSAGE_POSITION>,
444
+ align?: $Values<typeof MESSAGE_ALIGN>,
445
+ |};
446
+
447
+ export type ButtonMessageInputs = {|
448
+ amount?: number | void,
449
+ offer?: $ReadOnlyArray<$Values<typeof MESSAGE_OFFER>> | void,
450
+ color?: $Values<typeof MESSAGE_COLOR> | void,
451
+ position?: $Values<typeof MESSAGE_POSITION> | void,
452
+ align?: $Values<typeof MESSAGE_ALIGN> | void,
453
+ |};
454
+
435
455
  export type RenderButtonProps = {|
436
456
  style: ButtonStyle,
437
457
  locale: LocaleType,
@@ -466,6 +486,8 @@ export type RenderButtonProps = {|
466
486
  supportedNativeBrowser: boolean,
467
487
  showPayLabel: boolean,
468
488
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
489
+ message?: ButtonMessage,
490
+ messageMarkup?: string,
469
491
  |};
470
492
 
471
493
  export type PrerenderDetails = {|
@@ -525,6 +547,8 @@ export type ButtonProps = {|
525
547
  createVaultSetupToken: CreateVaultSetupToken,
526
548
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
527
549
  hostedButtonId?: string,
550
+ message?: ButtonMessage,
551
+ messageMarkup?: string,
528
552
  |};
529
553
 
530
554
  // eslint-disable-next-line flowtype/require-exact-type
@@ -567,6 +591,8 @@ export type ButtonPropsInputs = {
567
591
  supportedNativeBrowser: boolean,
568
592
  showPayLabel: boolean,
569
593
  displayOnly: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
594
+ message?: ButtonMessageInputs | void,
595
+ messageMarkup?: string | void,
570
596
  };
571
597
 
572
598
  export const DEFAULT_STYLE = {
@@ -715,6 +741,65 @@ export function normalizeButtonStyle(
715
741
  };
716
742
  }
717
743
 
744
+ export function normalizeButtonMessage(
745
+ props: ?ButtonPropsInputs,
746
+ message: ButtonMessageInputs
747
+ ): ButtonMessage {
748
+ const { amount, offer, color, position, align } = 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,
799
+ align,
800
+ };
801
+ }
802
+
718
803
  const COUNTRIES = values(COUNTRY);
719
804
  const FUNDING_SOURCES = values(FUNDING);
720
805
  const ENVS = values(ENV);
@@ -769,6 +854,8 @@ export function normalizeButtonProps(
769
854
  supportedNativeBrowser = false,
770
855
  showPayLabel = true,
771
856
  displayOnly = [],
857
+ message,
858
+ messageMarkup,
772
859
  } = props;
773
860
 
774
861
  const { country, lang } = locale;
@@ -860,5 +947,7 @@ export function normalizeButtonProps(
860
947
  supportedNativeBrowser,
861
948
  showPayLabel,
862
949
  displayOnly,
950
+ message: message ? normalizeButtonMessage(props, message) : undefined,
951
+ messageMarkup,
863
952
  };
864
953
  }
@@ -1,5 +1,41 @@
1
1
  /* @flow */
2
+ import { BUTTON_LAYOUT, MESSAGE_POSITION } from "../../constants";
3
+ import { ValidationError } from "../../lib";
4
+
5
+ import type { ButtonMessage } from "./props";
2
6
 
3
7
  export function isBorderRadiusNumber(borderRadius?: number): boolean {
4
8
  return typeof borderRadius === "number";
5
9
  }
10
+
11
+ type calculateMessagePositionProps = {|
12
+ message: ButtonMessage | void,
13
+ showPoweredBy: boolean,
14
+ layout: string,
15
+ |};
16
+
17
+ export function calculateMessagePosition({
18
+ message,
19
+ showPoweredBy,
20
+ layout,
21
+ }: calculateMessagePositionProps): string {
22
+ if (!message) {
23
+ return "none";
24
+ }
25
+ const { position } = message;
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>;
@@ -677,6 +682,92 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
677
682
  },
678
683
  },
679
684
 
685
+ onMessageReady: {
686
+ type: "function",
687
+ required: false,
688
+ value: ({ props }) => {
689
+ return async ({
690
+ offerType,
691
+ messageType,
692
+ offerCountryCode,
693
+ creditProductIdentifier,
694
+ }) => {
695
+ const { message, buttonSessionID } = props;
696
+ const amount = message?.amount || undefined;
697
+
698
+ getLogger()
699
+ .info("button_message_render")
700
+ .track({
701
+ [FPTI_KEY.EVENT_NAME]: "message_render",
702
+ // adding temp string here for our sdk constants
703
+ button_message_offer_type: offerType,
704
+ button_message_credit_product_identifier:
705
+ creditProductIdentifier,
706
+ button_message_type: messageType,
707
+ button_message_posiiton: message.position,
708
+ button_message_align: message.align,
709
+ button_message_color: message.color,
710
+ button_message_offer_country: offerCountryCode,
711
+ [FPTI_KEY.AMOUNT]: amount,
712
+ [FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
713
+ });
714
+ };
715
+ },
716
+ },
717
+
718
+ onMessageHover: {
719
+ type: "function",
720
+ required: false,
721
+ value: ({ props }) => {
722
+ return () => {
723
+ // lazy loads the modal, to be memoized and executed onMessageClick
724
+ const { clientID, merchantID } = props;
725
+ getModal(clientID, merchantID);
726
+ };
727
+ },
728
+ },
729
+
730
+ onMessageClick: {
731
+ type: "function",
732
+ required: false,
733
+ value: ({ props }) => {
734
+ return async ({
735
+ offerType,
736
+ messageType,
737
+ offerCountryCode,
738
+ creditProductIdentifier,
739
+ }) => {
740
+ const { message, clientID, merchantID, currency, buttonSessionID } =
741
+ props;
742
+ const amount = message?.amount || undefined;
743
+
744
+ const modalInstance = await getModal(clientID, merchantID);
745
+ modalInstance.show({
746
+ amount,
747
+ offer: offerType?.join(",") || undefined,
748
+ currency,
749
+ });
750
+
751
+ getLogger()
752
+ .info("button_message_clicked")
753
+ .track({
754
+ [FPTI_KEY.EVENT_NAME]: "message_click",
755
+ // adding temp string here for our sdk constants
756
+ button_message_offer_type: offerType,
757
+ button_message_credit_product_identifier:
758
+ creditProductIdentifier,
759
+ button_message_type: messageType,
760
+ button_message_posiiton: message.position,
761
+ button_message_align: message.align,
762
+ button_message_color: message.color,
763
+ button_message_offer_country: offerCountryCode,
764
+ [FPTI_KEY.AMOUNT]: amount,
765
+ [FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
766
+ });
767
+ };
768
+ },
769
+ },
770
+
680
771
  onShippingAddressChange: {
681
772
  type: "function",
682
773
  required: false,
@@ -887,6 +978,16 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
887
978
  required: false,
888
979
  default: () => window.__TEST_WALLET__,
889
980
  },
981
+
982
+ message: {
983
+ type: "object",
984
+ queryParam: true,
985
+ required: false,
986
+ decorate: ({ props, value }) => {
987
+ // $FlowFixMe
988
+ return normalizeButtonMessage(props, value);
989
+ },
990
+ },
890
991
  },
891
992
  });
892
993
  });
@@ -1,3 +1,5 @@
1
+ /* eslint-disable compat/compat */
2
+ /* eslint-disable promise/no-native */
1
3
  /* @flow */
2
4
  import {
3
5
  supportsPopups as userAgentSupportsPopups,
@@ -13,6 +15,7 @@ import {
13
15
  getElement,
14
16
  isStandAlone,
15
17
  once,
18
+ memoize,
16
19
  } from "@krakenjs/belter/src";
17
20
  import { FUNDING } from "@paypal/sdk-constants/src";
18
21
  import {
@@ -22,6 +25,8 @@ import {
22
25
  getFundingEligibility,
23
26
  getPlatform,
24
27
  getComponents,
28
+ getEnv,
29
+ getNamespace,
25
30
  } from "@paypal/sdk-client/src";
26
31
  import { getRefinedFundingEligibility } from "@paypal/funding-components/src";
27
32
 
@@ -357,3 +362,60 @@ export function getButtonSize(
357
362
  }
358
363
  }
359
364
  }
365
+
366
+ export const getModal: (
367
+ clientID: string,
368
+ merchantID: $ReadOnlyArray<string> | void
369
+ ) => Object = memoize(async (clientID, merchantID) => {
370
+ try {
371
+ const namespace = getNamespace();
372
+ if (!window[namespace]?.MessagesModal) {
373
+ const modalBundleUrl = () => {
374
+ let envPiece;
375
+ switch (getEnv()) {
376
+ case "local":
377
+ case "test":
378
+ case "stage":
379
+ envPiece = "stage";
380
+ break;
381
+ case "sandbox":
382
+ envPiece = "sandbox";
383
+ break;
384
+ case "production":
385
+ default:
386
+ envPiece = "js";
387
+ }
388
+ return `https://www.paypalobjects.com/upstream/bizcomponents/${envPiece}/modal.js`;
389
+ };
390
+
391
+ // eslint-disable-next-line no-restricted-globals
392
+ await new Promise((resolve, reject) => {
393
+ const script = document.createElement("script");
394
+ script.src = modalBundleUrl();
395
+ script.setAttribute("data-pp-namespace", namespace);
396
+ script.addEventListener("error", (err: Event) => {
397
+ reject(err);
398
+ });
399
+ script.addEventListener("load", () => {
400
+ document.body?.removeChild(script);
401
+ resolve();
402
+ });
403
+ document.body?.appendChild(script);
404
+ });
405
+ }
406
+
407
+ const modal = window[namespace].MessagesModal;
408
+ return modal({
409
+ account: `client-id:${clientID}`,
410
+ merchantId: merchantID?.join(",") || undefined,
411
+ });
412
+ } catch (err) {
413
+ getLogger()
414
+ .info("button_message_modal_fetch_error")
415
+ .track({
416
+ err: err.message || "BUTTON_MESSAGE_MODAL_FETCH_ERROR",
417
+ details: err.details,
418
+ stack: JSON.stringify(err.stack || err),
419
+ });
420
+ }
421
+ });
@@ -1,25 +0,0 @@
1
- /* @flow */
2
- import { getLogger } from "@paypal/sdk-client/src";
3
-
4
- // TODO: This will be pulled in to a shared sdk-client util
5
- export const sendCountMetric = ({
6
- dimensions,
7
- event = "unused",
8
- name,
9
- value = 1,
10
- }: {|
11
- event?: string,
12
- name: string,
13
- value?: number,
14
- dimensions: {
15
- [string]: mixed,
16
- },
17
- // $FlowIssue return type
18
- |}) =>
19
- getLogger().metric({
20
- dimensions,
21
- metricEventName: event,
22
- metricNamespace: name,
23
- metricValue: value,
24
- metricType: "counter",
25
- });