@paypal/checkout-components 5.0.381 → 5.0.383-alpha-0983e51.0

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,
@@ -50,6 +51,7 @@ type MarksProps = {|
50
51
  onShippingAddressChange?: OnShippingAddressChange,
51
52
  onShippingOptionsChange?: OnShippingOptionsChange,
52
53
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
54
+ userAgent: string,
53
55
  |};
54
56
 
55
57
  export type MarksComponent = (MarksProps) => MarksInstance;
@@ -61,6 +63,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
61
63
  onShippingAddressChange,
62
64
  onShippingOptionsChange,
63
65
  displayOnly,
66
+ userAgent = getUserAgent(),
64
67
  }: MarksProps = {}): MarksInstance {
65
68
  const height = DEFAULT_HEIGHT;
66
69
  const fundingEligibility = getFundingEligibility();
@@ -97,6 +100,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
97
100
  supportedNativeBrowser,
98
101
  experiment,
99
102
  displayOnly,
103
+ userAgent,
100
104
  });
101
105
  const env = getEnv();
102
106
 
@@ -121,6 +125,7 @@ export const getMarksComponent: () => MarksComponent = memoize(() => {
121
125
  supportedNativeBrowser,
122
126
  experiment,
123
127
  displayOnly,
128
+ userAgent,
124
129
  });
125
130
  };
126
131
 
@@ -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
 
@@ -491,6 +491,7 @@ export type RenderButtonProps = {|
491
491
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
492
492
  message?: ButtonMessage,
493
493
  messageMarkup?: string,
494
+ userAgent: string,
494
495
  |};
495
496
 
496
497
  export type PrerenderDetails = {|
@@ -633,6 +634,7 @@ export type ButtonProps = {|
633
634
  message?: ButtonMessage,
634
635
  messageMarkup?: string,
635
636
  hideSubmitButtonForCardForm?: boolean,
637
+ userAgent: string,
636
638
  |};
637
639
 
638
640
  // eslint-disable-next-line flowtype/require-exact-type
@@ -683,6 +685,7 @@ export type ButtonPropsInputs = {
683
685
  messageMarkup?: string | void,
684
686
  renderedButtons: $ReadOnlyArray<$Values<typeof FUNDING>>,
685
687
  buttonColor: ButtonColor,
688
+ userAgent: string,
686
689
  };
687
690
 
688
691
  export const DEFAULT_STYLE = {
@@ -1262,6 +1265,7 @@ export function normalizeButtonProps(
1262
1265
  messageMarkup,
1263
1266
  renderedButtons,
1264
1267
  shopperSessionId,
1268
+ userAgent,
1265
1269
  } = props;
1266
1270
 
1267
1271
  const { country, lang } = locale;
@@ -1315,6 +1319,7 @@ export function normalizeButtonProps(
1315
1319
  supportsPopups,
1316
1320
  supportedNativeBrowser,
1317
1321
  displayOnly,
1322
+ userAgent,
1318
1323
  })
1319
1324
  ) {
1320
1325
  throw new Error(`Funding Source not eligible: ${fundingSource}`);
@@ -1366,5 +1371,6 @@ export function normalizeButtonProps(
1366
1371
  displayOnly,
1367
1372
  message,
1368
1373
  messageMarkup,
1374
+ userAgent,
1369
1375
  };
1370
1376
  }
@@ -211,7 +211,7 @@ export const buttonRebrandStyle = `
211
211
  width: auto;
212
212
  }
213
213
 
214
- // Hide/show credit/paylater logos based on button width
214
+ /* Hide/show credit/paylater logos based on button width */
215
215
  @media only screen and (min-width: 0px) and (max-width: 150px) {
216
216
  [${ATTRIBUTE.FUNDING_SOURCE}="credit"].${CLASS.BUTTON_REBRAND} .${CLASS.BUTTON_LABEL} .${CLASS.LOGO_REBRAND},
217
217
  [${ATTRIBUTE.FUNDING_SOURCE}="paylater"].${CLASS.BUTTON_REBRAND} .${CLASS.BUTTON_LABEL} .${CLASS.LOGO_REBRAND} {
@@ -61,6 +61,7 @@ import {
61
61
  isApplePaySupported,
62
62
  supportsPopups as userAgentSupportsPopups,
63
63
  noop,
64
+ getUserAgent,
64
65
  } from "@krakenjs/belter/src";
65
66
  import {
66
67
  FUNDING,
@@ -89,6 +90,10 @@ import {
89
90
  type ButtonExtensions,
90
91
  } from "../../ui/buttons/props";
91
92
  import { isFundingEligible } from "../../funding";
93
+ import {
94
+ supportsVenmoPopups,
95
+ isSupportedNativeVenmoBrowser,
96
+ } from "../../funding/util";
92
97
  import { getPixelComponent } from "../pixel";
93
98
  import { CLASS } from "../../constants";
94
99
  import { PayPalAppSwitchOverlay } from "../../ui/overlay/paypal-app-switch/overlay";
@@ -251,6 +256,7 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
251
256
  createSubscription,
252
257
  createVaultSetupToken,
253
258
  displayOnly,
259
+ userAgent,
254
260
  } = props;
255
261
 
256
262
  const flow = determineFlow({
@@ -294,6 +300,7 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
294
300
  supportedNativeBrowser,
295
301
  experiment,
296
302
  displayOnly,
303
+ userAgent,
297
304
  })
298
305
  ) {
299
306
  return {
@@ -714,6 +721,7 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
714
721
  createSubscription,
715
722
  createVaultSetupToken,
716
723
  displayOnly,
724
+ userAgent = getUserAgent(),
717
725
  } = props;
718
726
 
719
727
  const flow = determineFlow({
@@ -744,6 +752,7 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
744
752
  supportsPopups,
745
753
  supportedNativeBrowser,
746
754
  displayOnly,
755
+ userAgent,
747
756
  })
748
757
  ) {
749
758
  throw new Error(`${fundingSource} is not eligible`);
@@ -1266,13 +1275,28 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
1266
1275
 
1267
1276
  supportedNativeBrowser: {
1268
1277
  type: "boolean",
1269
- value: isSupportedNativeBrowser,
1278
+ value: ({ props }) => {
1279
+ if (props.fundingSource === FUNDING.VENMO) {
1280
+ return isSupportedNativeVenmoBrowser(
1281
+ props.experiment,
1282
+ props.userAgent
1283
+ );
1284
+ }
1285
+
1286
+ return isSupportedNativeBrowser();
1287
+ },
1270
1288
  queryParam: true,
1271
1289
  },
1272
1290
 
1273
1291
  supportsPopups: {
1274
1292
  type: "boolean",
1275
- value: () => userAgentSupportsPopups(),
1293
+ value: ({ props }) => {
1294
+ if (props.fundingSource === FUNDING.VENMO) {
1295
+ return supportsVenmoPopups(props.experiment, props.userAgent);
1296
+ }
1297
+
1298
+ return userAgentSupportsPopups();
1299
+ },
1276
1300
  queryParam: true,
1277
1301
  },
1278
1302
 
@@ -1316,6 +1340,13 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
1316
1340
  required: false,
1317
1341
  queryParam: true,
1318
1342
  },
1343
+
1344
+ userAgent: {
1345
+ type: "string",
1346
+ required: false,
1347
+ queryParam: true,
1348
+ value: getUserAgent,
1349
+ },
1319
1350
  },
1320
1351
 
1321
1352
  exports: {