@paypal/checkout-components 5.0.383 → 5.0.385

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.
@@ -0,0 +1,264 @@
1
+ /* @flow */
2
+
3
+ import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
4
+ import {
5
+ isWebView,
6
+ isIosWebview,
7
+ isAndroidWebview,
8
+ isFacebookWebView,
9
+ isOperaMini,
10
+ isFirefoxIOS,
11
+ isEdgeIOS,
12
+ isQQBrowser,
13
+ isElectron,
14
+ supportsPopups,
15
+ isTablet,
16
+ isIos,
17
+ isSafari,
18
+ isAndroid,
19
+ isChrome,
20
+ isFirefox,
21
+ } from "@krakenjs/belter/src";
22
+
23
+ import { supportsVenmoPopups, isSupportedNativeVenmoBrowser } from "./util";
24
+
25
+ // Mock all the browser detection functions from belter
26
+ vi.mock("@krakenjs/belter/src", () => ({
27
+ isWebView: vi.fn(),
28
+ isIosWebview: vi.fn(),
29
+ isAndroidWebview: vi.fn(),
30
+ isFacebookWebView: vi.fn(),
31
+ isOperaMini: vi.fn(),
32
+ isFirefoxIOS: vi.fn(),
33
+ isEdgeIOS: vi.fn(),
34
+ isQQBrowser: vi.fn(),
35
+ isElectron: vi.fn(),
36
+ supportsPopups: vi.fn(),
37
+ isTablet: vi.fn(),
38
+ isIos: vi.fn(),
39
+ isSafari: vi.fn(),
40
+ isAndroid: vi.fn(),
41
+ isChrome: vi.fn(),
42
+ isFirefox: vi.fn(),
43
+ }));
44
+
45
+ describe("funding/util", () => {
46
+ const defaultUserAgent =
47
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
48
+
49
+ beforeEach(() => {
50
+ // Reset all mocks before each test
51
+ vi.clearAllMocks();
52
+
53
+ // Reset window.popupBridge
54
+ delete window.popupBridge;
55
+
56
+ // Set default mock return values
57
+ vi.mocked(isWebView).mockReturnValue(false);
58
+ vi.mocked(isIosWebview).mockReturnValue(false);
59
+ vi.mocked(isAndroidWebview).mockReturnValue(false);
60
+ vi.mocked(isFacebookWebView).mockReturnValue(false);
61
+ vi.mocked(isOperaMini).mockReturnValue(false);
62
+ vi.mocked(isFirefoxIOS).mockReturnValue(false);
63
+ vi.mocked(isEdgeIOS).mockReturnValue(false);
64
+ vi.mocked(isQQBrowser).mockReturnValue(false);
65
+ vi.mocked(isElectron).mockReturnValue(false);
66
+ vi.mocked(supportsPopups).mockReturnValue(true);
67
+ vi.mocked(isTablet).mockReturnValue(false);
68
+ vi.mocked(isIos).mockReturnValue(false);
69
+ vi.mocked(isSafari).mockReturnValue(false);
70
+ vi.mocked(isAndroid).mockReturnValue(false);
71
+ vi.mocked(isChrome).mockReturnValue(false);
72
+ vi.mocked(isFirefox).mockReturnValue(false);
73
+ });
74
+
75
+ afterEach(() => {
76
+ vi.resetAllMocks();
77
+ });
78
+
79
+ describe("supportsVenmoPopups", () => {
80
+ describe("when in supported webview", () => {
81
+ beforeEach(() => {
82
+ vi.mocked(isWebView).mockReturnValue(true);
83
+ });
84
+
85
+ it("should return true when popupBridge is available", () => {
86
+ window.popupBridge = {};
87
+
88
+ expect(supportsVenmoPopups({}, defaultUserAgent)).toBe(true);
89
+ });
90
+
91
+ it("should return false when popupBridge is not available", () => {
92
+ expect(supportsVenmoPopups({}, defaultUserAgent)).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe("when not in supported webview", () => {
97
+ it("should return true when experiment flag is enabled and user agent supports popups", () => {
98
+ vi.mocked(supportsPopups).mockReturnValue(true);
99
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
100
+
101
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(true);
102
+ });
103
+
104
+ it("should return false when experiment flag is enabled but user agent doesn't support popups due to isOperaMini", () => {
105
+ vi.mocked(isOperaMini).mockReturnValue(true);
106
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
107
+
108
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
109
+ });
110
+
111
+ it("should return false when experiment flag is enabled but user agent doesn't support popups due to isFirefoxIOS", () => {
112
+ vi.mocked(isFirefoxIOS).mockReturnValue(true);
113
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
114
+
115
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
116
+ });
117
+
118
+ it("should return false when experiment flag is enabled but user agent doesn't support popups due to isEdgeIOS", () => {
119
+ vi.mocked(isEdgeIOS).mockReturnValue(true);
120
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
121
+
122
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
123
+ });
124
+
125
+ it("should return false when experiment flag is enabled but user agent doesn't support popups due to isQQBrowser", () => {
126
+ vi.mocked(isQQBrowser).mockReturnValue(true);
127
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
128
+
129
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
130
+ });
131
+
132
+ it("should return false when experiment flag is enabled but user agent doesn't support popups due to isElectron", () => {
133
+ vi.mocked(isElectron).mockReturnValue(true);
134
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
135
+
136
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
137
+ });
138
+
139
+ it("should fall back to supportsPopups when experiment flag is not enabled", () => {
140
+ vi.mocked(supportsPopups).mockReturnValue(true);
141
+ const experiment = {};
142
+
143
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(true);
144
+ });
145
+
146
+ it("should fall back to supportsPopups when experiment flag is false", () => {
147
+ vi.mocked(supportsPopups).mockReturnValue(false);
148
+ const experiment = { venmoEnableWebOnNonNativeBrowser: false };
149
+
150
+ expect(supportsVenmoPopups(experiment, defaultUserAgent)).toBe(false);
151
+ });
152
+
153
+ it("should handle undefined experiment", () => {
154
+ vi.mocked(supportsPopups).mockReturnValue(true);
155
+
156
+ expect(supportsVenmoPopups(undefined, defaultUserAgent)).toBe(true);
157
+ });
158
+ });
159
+ });
160
+
161
+ describe("isSupportedNativeVenmoBrowser", () => {
162
+ describe("when in supported webview", () => {
163
+ beforeEach(() => {
164
+ vi.mocked(isWebView).mockReturnValue(true);
165
+ });
166
+
167
+ it("should return true when popupBridge is available", () => {
168
+ window.popupBridge = {};
169
+
170
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(true);
171
+ });
172
+
173
+ it("should return false when popupBridge is not available", () => {
174
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe("when not in supported webview", () => {
179
+ it("should return false when on tablet", () => {
180
+ vi.mocked(isTablet).mockReturnValue(true);
181
+
182
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
183
+ });
184
+
185
+ it("should return true for iOS Safari by default", () => {
186
+ vi.mocked(isIos).mockReturnValue(true);
187
+ vi.mocked(isSafari).mockReturnValue(true);
188
+
189
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(true);
190
+ });
191
+
192
+ it("should return true for Android Chrome by default", () => {
193
+ vi.mocked(isAndroid).mockReturnValue(true);
194
+ vi.mocked(isChrome).mockReturnValue(true);
195
+
196
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(true);
197
+ });
198
+
199
+ it("should return false for iOS Chrome without experiment flag", () => {
200
+ vi.mocked(isIos).mockReturnValue(true);
201
+ vi.mocked(isChrome).mockReturnValue(true);
202
+
203
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
204
+ });
205
+
206
+ it("should return true for iOS Chrome with experiment flag", () => {
207
+ vi.mocked(isIos).mockReturnValue(true);
208
+ vi.mocked(isChrome).mockReturnValue(true);
209
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
210
+
211
+ expect(
212
+ isSupportedNativeVenmoBrowser(experiment, defaultUserAgent)
213
+ ).toBe(true);
214
+ });
215
+
216
+ it("should return false for Android Firefox without experiment flag", () => {
217
+ vi.mocked(isAndroid).mockReturnValue(true);
218
+ vi.mocked(isFirefox).mockReturnValue(true);
219
+
220
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
221
+ });
222
+
223
+ it("should return true for Android Firefox with experiment flag", () => {
224
+ vi.mocked(isAndroid).mockReturnValue(true);
225
+ vi.mocked(isFirefox).mockReturnValue(true);
226
+ const experiment = { venmoEnableWebOnNonNativeBrowser: true };
227
+
228
+ expect(
229
+ isSupportedNativeVenmoBrowser(experiment, defaultUserAgent)
230
+ ).toBe(true);
231
+ });
232
+
233
+ it("should return false for unsupported browser combinations", () => {
234
+ vi.mocked(isIos).mockReturnValue(true);
235
+ vi.mocked(isFirefox).mockReturnValue(true); // iOS Firefox (not Chrome)
236
+
237
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
238
+ });
239
+
240
+ it("should return false for desktop browsers", () => {
241
+ // No mobile flags set, simulating desktop
242
+
243
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
244
+ });
245
+
246
+ it("should handle undefined experiment", () => {
247
+ vi.mocked(isIos).mockReturnValue(true);
248
+ vi.mocked(isSafari).mockReturnValue(true);
249
+
250
+ expect(isSupportedNativeVenmoBrowser(undefined, defaultUserAgent)).toBe(
251
+ true
252
+ );
253
+ });
254
+
255
+ it("should prioritize tablet check over browser checks", () => {
256
+ vi.mocked(isTablet).mockReturnValue(true);
257
+ vi.mocked(isIos).mockReturnValue(true);
258
+ vi.mocked(isSafari).mockReturnValue(true);
259
+
260
+ expect(isSupportedNativeVenmoBrowser({}, defaultUserAgent)).toBe(false);
261
+ });
262
+ });
263
+ });
264
+ });
@@ -48,19 +48,18 @@ export function getVenmoConfig(): FundingSourceConfig {
48
48
  return true;
49
49
  },
50
50
 
51
- requires: ({ experiment, platform }) => {
52
- const isNonNativeSupported =
53
- experiment?.venmoEnableWebOnNonNativeBrowser === true ||
54
- (__WEB__ && window.popupBridge);
55
-
51
+ requires: ({ platform }) => {
56
52
  if (platform === PLATFORM.MOBILE) {
57
53
  return {
58
- native: isNonNativeSupported ? false : true,
59
- popup: isNonNativeSupported ? false : true,
54
+ native: true,
55
+ popup: true,
60
56
  };
61
57
  }
62
58
 
63
- return {};
59
+ return {
60
+ native: false,
61
+ popup: false,
62
+ };
64
63
  },
65
64
 
66
65
  Logo: ({ logoColor, optional, shouldApplyRebrandedStyles }) => {
@@ -8,6 +8,9 @@ import { BUTTON_FLOW } from "../../constants";
8
8
  import { getVenmoConfig } from "./config";
9
9
 
10
10
  describe("Venmo eligibility", () => {
11
+ window.navigator.mockUserAgent =
12
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
13
+
11
14
  const baseEligibilityProps = {
12
15
  fundingSource: undefined,
13
16
  components: ["buttons"],
@@ -102,58 +105,35 @@ describe("Venmo eligibility", () => {
102
105
  });
103
106
 
104
107
  describe("requires", () => {
105
- test("should not check for native or popup eligibility if platform is mobile and window.popupBridge is defined", () => {
106
- window.popupBridge = {};
107
-
108
- const isVenmoEligible = venmoConfig.requires?.({
109
- experiment: {
110
- venmoEnableWebOnNonNativeBrowser: true,
111
- },
108
+ test("should require native and popup support when platform is mobile", () => {
109
+ const venmoRequires = venmoConfig.requires?.({
112
110
  platform: PLATFORM.MOBILE,
113
111
  });
114
112
 
115
- expect(isVenmoEligible).toEqual({
116
- native: false,
117
- popup: false,
113
+ expect(venmoRequires).toEqual({
114
+ native: true,
115
+ popup: true,
118
116
  });
119
-
120
- window.popupBridge = undefined;
121
117
  });
122
118
 
123
- test("should not check for native or popup eligibility if platform is mobile and venmoEnableWebOnNonNativeBrowser is true", () => {
124
- const isVenmoEligible = venmoConfig.requires?.({
125
- experiment: {
126
- venmoEnableWebOnNonNativeBrowser: true,
127
- },
128
- platform: PLATFORM.MOBILE,
119
+ test("should not require native or popup support when platform is desktop", () => {
120
+ const venmoRequires = venmoConfig.requires?.({
121
+ platform: PLATFORM.DESKTOP,
129
122
  });
130
123
 
131
- expect(isVenmoEligible).toEqual({
124
+ expect(venmoRequires).toEqual({
132
125
  native: false,
133
126
  popup: false,
134
127
  });
135
128
  });
136
129
 
137
- test("should check for native and popup eligibility if platform is mobile and venmoEnableWebOnNonNativeBrowser is false and window.popupBridge is not defined", () => {
138
- const isVenmoEligible = venmoConfig.requires?.({
139
- experiment: {
140
- venmoEnableWebOnNonNativeBrowser: false,
141
- },
142
- platform: PLATFORM.MOBILE,
143
- });
130
+ test("should not require native or popup support when platform is not specified", () => {
131
+ const venmoRequires = venmoConfig.requires?.({});
144
132
 
145
- expect(isVenmoEligible).toEqual({
146
- native: true,
147
- popup: true,
148
- });
149
- });
150
-
151
- test("should not check for native and popup eligibility if platform is not mobile", () => {
152
- const isVenmoEligible = venmoConfig.requires?.({
153
- platform: PLATFORM.DESKTOP,
133
+ expect(venmoRequires).toEqual({
134
+ native: false,
135
+ popup: false,
154
136
  });
155
-
156
- expect(isVenmoEligible).toEqual({});
157
137
  });
158
138
  });
159
139
  });
@@ -9,6 +9,7 @@ import {
9
9
  memoize,
10
10
  isApplePaySupported,
11
11
  supportsPopups as userAgentSupportsPopups,
12
+ getUserAgent,
12
13
  } from "@krakenjs/belter/src";
13
14
  import {
14
15
  PLATFORM,
@@ -32,10 +33,11 @@ import { BUTTON_LAYOUT, BUTTON_FLOW } from "../constants";
32
33
  import { determineEligibleFunding, isFundingEligible } from "../funding";
33
34
  import {
34
35
  isSupportedNativeBrowser,
35
- getVenmoEligibility,
36
+ getButtonExperiments,
36
37
  } from "../zoid/buttons/util";
37
38
 
38
39
  import { MarksElement } from "./template";
40
+ import { MarksElementRebrand } from "./templateRebrand";
39
41
 
40
42
  const DEFAULT_HEIGHT = 20;
41
43
 
@@ -50,6 +52,7 @@ type MarksProps = {|
50
52
  onShippingAddressChange?: OnShippingAddressChange,
51
53
  onShippingOptionsChange?: OnShippingOptionsChange,
52
54
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
55
+ userAgent: string,
53
56
  |};
54
57
 
55
58
  export type MarksComponent = (MarksProps) => MarksInstance;
@@ -61,6 +64,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
61
64
  onShippingAddressChange,
62
65
  onShippingOptionsChange,
63
66
  displayOnly,
67
+ userAgent = getUserAgent(),
64
68
  }: MarksProps = {}): MarksInstance {
65
69
  const height = DEFAULT_HEIGHT;
66
70
  const fundingEligibility = getFundingEligibility();
@@ -75,7 +79,8 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
75
79
  : false;
76
80
  const supportsPopups = userAgentSupportsPopups();
77
81
  const supportedNativeBrowser = isSupportedNativeBrowser();
78
- const experiment = getVenmoEligibility();
82
+ const experiment = getButtonExperiments();
83
+
79
84
  const hasShippingCallback = Boolean(
80
85
  onShippingChange || onShippingAddressChange || onShippingOptionsChange
81
86
  );
@@ -97,6 +102,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
97
102
  supportedNativeBrowser,
98
103
  experiment,
99
104
  displayOnly,
105
+ userAgent,
100
106
  });
101
107
  const env = getEnv();
102
108
 
@@ -121,6 +127,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
121
127
  supportedNativeBrowser,
122
128
  experiment,
123
129
  displayOnly,
130
+ userAgent,
124
131
  });
125
132
  };
126
133
 
@@ -130,16 +137,28 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
130
137
  throw new Error(`${fundingSource || "marks"} not eligible`);
131
138
  }
132
139
 
140
+ const isRebrandEnabled = experiment?.isPaypalRebrandEnabled;
141
+
133
142
  getElement(container).appendChild(
134
143
  (
135
144
  <div>
136
- <MarksElement
137
- fundingEligibility={fundingEligibility}
138
- fundingSources={fundingSources}
139
- height={height}
140
- experiment={experiment}
141
- env={env}
142
- />
145
+ {isRebrandEnabled ? (
146
+ <MarksElementRebrand
147
+ fundingEligibility={fundingEligibility}
148
+ fundingSources={fundingSources}
149
+ height={height}
150
+ experiment={experiment}
151
+ env={env}
152
+ />
153
+ ) : (
154
+ <MarksElement
155
+ fundingEligibility={fundingEligibility}
156
+ fundingSources={fundingSources}
157
+ height={height}
158
+ experiment={experiment}
159
+ env={env}
160
+ />
161
+ )}
143
162
  </div>
144
163
  ).render(dom({ doc: document }))
145
164
  );
@@ -33,19 +33,14 @@ function Mark({
33
33
  throw new Error(`Can not find funding config for ${fundingSource}`);
34
34
  }
35
35
 
36
- const { Logo } = fundingConfig;
36
+ const { Logo, shouldUseMarkForRebrandOnly } = fundingConfig;
37
37
  const MarkLogo = fundingConfig.Mark;
38
38
  const marksDefined = typeof MarkLogo !== "undefined";
39
39
 
40
40
  return (
41
41
  <div class="paypal-mark">
42
- {marksDefined && MarkLogo ? (
43
- <MarkLogo
44
- fundingEligibility={fundingEligibility}
45
- locale={getLocale()}
46
- experiment={experiment}
47
- env={env}
48
- />
42
+ {marksDefined && MarkLogo && !shouldUseMarkForRebrandOnly ? (
43
+ <MarkLogo />
49
44
  ) : (
50
45
  <Logo
51
46
  fundingEligibility={fundingEligibility}
@@ -0,0 +1,177 @@
1
+ /* @flow */
2
+ /** @jsx node */
3
+
4
+ import { FUNDING, ENV } from "@paypal/sdk-constants/src";
5
+ import {
6
+ node,
7
+ type ChildNodeType,
8
+ type ElementNode,
9
+ } from "@krakenjs/jsx-pragmatic/src";
10
+ import { getLocale, type FundingEligibilityType } from "@paypal/sdk-client/src";
11
+ import { toPx } from "@krakenjs/belter/src";
12
+
13
+ import type { Experiment } from "../types";
14
+ import { getFundingConfig } from "../funding";
15
+ import { CLASS } from "../constants";
16
+
17
+ type MarkOptions = {|
18
+ fundingSource: $Values<typeof FUNDING>,
19
+ fundingEligibility: FundingEligibilityType,
20
+ experiment: Experiment,
21
+ env: $Values<typeof ENV>,
22
+ |};
23
+
24
+ function Mark({
25
+ fundingSource,
26
+ fundingEligibility,
27
+ experiment,
28
+ env,
29
+ }: MarkOptions): ChildNodeType {
30
+ const fundingConfig = getFundingConfig()[fundingSource];
31
+
32
+ if (!fundingConfig) {
33
+ throw new Error(`Can not find funding config for ${fundingSource}`);
34
+ }
35
+
36
+ const { Logo } = fundingConfig;
37
+ const MarkLogo = fundingConfig.Mark;
38
+ const marksDefined = typeof MarkLogo !== "undefined";
39
+
40
+ let backgroundClasses = "paypal-mark-rebrand";
41
+
42
+ const hasBlueBackground =
43
+ fundingSource === FUNDING.PAYPAL ||
44
+ fundingSource === FUNDING.PAYLATER ||
45
+ fundingSource === FUNDING.CREDIT;
46
+
47
+ backgroundClasses += hasBlueBackground
48
+ ? " paypal-mark-rebrand-blue"
49
+ : " paypal-mark-rebrand-white";
50
+
51
+ const shouldUseOwnBorderAndPadding = fundingSource === FUNDING.APPLEPAY;
52
+ if (shouldUseOwnBorderAndPadding) {
53
+ backgroundClasses += " paypal-mark-rebrand-own-border-and-padding";
54
+ }
55
+
56
+ return (
57
+ <div class={backgroundClasses}>
58
+ {marksDefined && MarkLogo ? (
59
+ <MarkLogo shouldApplyRebrandedStyles={true} />
60
+ ) : (
61
+ <Logo
62
+ fundingEligibility={fundingEligibility}
63
+ locale={getLocale()}
64
+ experiment={experiment}
65
+ env={env}
66
+ shouldApplyRebrandedStyles={true}
67
+ />
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ type MarksElementOptions = {|
74
+ fundingEligibility: FundingEligibilityType,
75
+ fundingSources: $ReadOnlyArray<$Values<typeof FUNDING>>,
76
+ height: number,
77
+ experiment: Experiment,
78
+ env: $Values<typeof ENV>,
79
+ |};
80
+
81
+ export function MarksElementRebrand({
82
+ fundingEligibility,
83
+ fundingSources,
84
+ experiment,
85
+ env,
86
+ }: MarksElementOptions): ElementNode {
87
+ // Rebrand dimensions: 32px height, 48px width
88
+ const rebrandHeight = 32;
89
+ const rebrandWidth = 48;
90
+
91
+ return (
92
+ <div>
93
+ <style>
94
+ {`
95
+ .${CLASS.TEXT} {
96
+ font-family: PayPal Pro Book, system-ui, -apple-system, Roboto, "Segoe UI", Helvetica-Neue, Helvetica, Arial, sans-serif;
97
+ font-size: 12px;
98
+ vertical-align: middle;
99
+ }
100
+
101
+ .paypal-marks-rebrand {
102
+ display: flex;
103
+ }
104
+
105
+ .paypal-mark-rebrand {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ border-radius: 3px;
110
+ margin: ${toPx(rebrandHeight / 5)};
111
+ position: relative;
112
+ width: ${toPx(rebrandWidth)};
113
+ height: ${toPx(rebrandHeight)};
114
+ padding: 3px;
115
+ box-sizing: border-box;
116
+ overflow: hidden;
117
+ }
118
+
119
+ .paypal-mark-rebrand:last-child {
120
+ margin-right: none;
121
+ }
122
+
123
+ .paypal-mark-rebrand-white {
124
+ background: #fff;
125
+ border: 1px solid #E6E6E6;
126
+ }
127
+
128
+ .paypal-mark-rebrand-blue {
129
+ background: #60CDFF;
130
+ border: 1px solid #60CDFF;
131
+ }
132
+
133
+ .paypal-mark-rebrand-own-border-and-padding {
134
+ border: none;
135
+ padding: 0px;
136
+ }
137
+
138
+ .paypal-mark-rebrand img {
139
+ max-width: 100%;
140
+ max-height: 100%;
141
+ width: auto;
142
+ height: auto;
143
+ object-fit: contain;
144
+ display: block;
145
+ }
146
+
147
+ .paypal-button-card {
148
+ display: inline-block;
149
+ margin-right: ${toPx(rebrandHeight / 4)};
150
+ }
151
+
152
+ .paypal-button-card:last-child {
153
+ margin-right: 0px;
154
+ }
155
+
156
+ .paypal-mark-rebrand .paypal-logo {
157
+ margin-right: ${toPx(rebrandHeight / 5)};
158
+ }
159
+
160
+ .paypal-mark-rebrand .paypal-logo:last-child {
161
+ margin-right: 0px;
162
+ }
163
+ `}
164
+ </style>
165
+ <div class="paypal-marks-rebrand">
166
+ {fundingSources.map((fundingSource) => (
167
+ <Mark
168
+ fundingEligibility={fundingEligibility}
169
+ fundingSource={fundingSource}
170
+ experiment={experiment}
171
+ env={env}
172
+ />
173
+ ))}
174
+ </div>
175
+ </div>
176
+ );
177
+ }
@@ -168,6 +168,7 @@ export function Buttons(props: ButtonsProps): ElementNode {
168
168
  userIDToken,
169
169
  vault,
170
170
  wallet,
171
+ userAgent,
171
172
  } = normalizeButtonProps(props);
172
173
  const { layout, shape, tagline } = style;
173
174
 
@@ -190,6 +191,7 @@ export function Buttons(props: ButtonsProps): ElementNode {
190
191
  supportedNativeBrowser,
191
192
  experiment,
192
193
  displayOnly,
194
+ userAgent,
193
195
  });
194
196
  const multiple = fundingSources.length > 1;
195
197