@paypal/checkout-components 5.0.386 → 5.0.388

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.386",
3
+ "version": "5.0.388",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -23,6 +23,7 @@
23
23
  "jest-ssr": "jest test/ssr --env=node --no-cache --collectCoverage --collectCoverageFrom='src/' --coverageDirectory='coverage/jest'",
24
24
  "karma": "cross-env NODE_ENV=test babel-node ./node_modules/.bin/karma start",
25
25
  "lint": "eslint --ext .js --ext .jsx src/ test/ *.js",
26
+ "lint:fix": "eslint --ext .js --ext .jsx src/ test/ *.js --fix",
26
27
  "postversion": "./scripts/postversion.sh",
27
28
  "preversion": "./scripts/preversion.sh",
28
29
  "reinstall": "rimraf flow-typed && rimraf node_modules && npm install && npm run flow-typed",
@@ -141,6 +141,7 @@ export function getCardConfig(): FundingSourceConfig {
141
141
  secondaryColors: {
142
142
  ...DEFAULT_FUNDING_CONFIG.secondaryColors,
143
143
  [DEFAULT]: BUTTON_COLOR.BLACK,
144
+ [BUTTON_COLOR.REBRAND_BLUE]: BUTTON_COLOR.REBRAND_BLACK,
144
145
  },
145
146
 
146
147
  logoColors: {
@@ -22,8 +22,12 @@ import {
22
22
  DEFAULT,
23
23
  BUTTON_FLOW,
24
24
  } from "../../constants";
25
- import { DEFAULT_FUNDING_CONFIG, type FundingSourceConfig } from "../common";
26
- import { WalletLabel, Logo } from "../paypal/template";
25
+ import {
26
+ DEFAULT_FUNDING_CONFIG,
27
+ type FundingSourceConfig,
28
+ BasicLabel,
29
+ } from "../common";
30
+ import { WalletLabel, Logo as PayPalRebrandLogo } from "../paypal/template";
27
31
  import { Text } from "../../ui/text";
28
32
 
29
33
  import css from "./style.scoped.scss";
@@ -40,6 +44,14 @@ export function getCreditConfig(): FundingSourceConfig {
40
44
 
41
45
  layouts: [BUTTON_LAYOUT.HORIZONTAL, BUTTON_LAYOUT.VERTICAL],
42
46
 
47
+ Label: ({ logo, experiment, ...props }) => {
48
+ // For rebrand, only show logo without labels
49
+ if (experiment?.isPaypalRebrandEnabled) {
50
+ return logo;
51
+ }
52
+ return BasicLabel({ logo, ...props });
53
+ },
54
+
43
55
  Logo: ({
44
56
  locale,
45
57
  logoColor,
@@ -74,9 +86,31 @@ export function getCreditConfig(): FundingSourceConfig {
74
86
  );
75
87
  }
76
88
 
89
+ // Rebranded credit for DE locale uses "Später Bezahlen" text
90
+ if (locale.country === COUNTRY.DE) {
91
+ return (
92
+ <Style css={css} nonce={nonce}>
93
+ <PayPalRebrandLogo
94
+ logoColor={logoColor}
95
+ shouldApplyRebrandedStyles={shouldApplyRebrandedStyles}
96
+ env={env}
97
+ experiment={experiment}
98
+ fundingEligibility={fundingEligibility}
99
+ locale={locale}
100
+ />
101
+ {__WEB__ ? (
102
+ <PPRebrandLogoExternalImage logoColor={logoColorPP} />
103
+ ) : (
104
+ <PPRebrandLogoInlineSVG logoColor={logoColorPP} />
105
+ )}
106
+ <Text>{"Später Bezahlen"}</Text>
107
+ </Style>
108
+ );
109
+ }
110
+
77
111
  return (
78
112
  <Style css={css} nonce={nonce}>
79
- <Logo
113
+ <PayPalRebrandLogo
80
114
  logoColor={logoColor}
81
115
  shouldApplyRebrandedStyles={shouldApplyRebrandedStyles}
82
116
  locale={locale}
@@ -16,6 +16,7 @@ import type { Wallet, Experiment } from "../types";
16
16
  import { BUTTON_LAYOUT, BUTTON_FLOW } from "../constants";
17
17
 
18
18
  import { getFundingConfig } from "./config";
19
+ import { supportsVenmoPopups, isSupportedNativeVenmoBrowser } from "./util";
19
20
 
20
21
  type IsFundingEligibleOptions = {|
21
22
  layout?: $Values<typeof BUTTON_LAYOUT>,
@@ -35,6 +36,7 @@ type IsFundingEligibleOptions = {|
35
36
  supportedNativeBrowser: boolean,
36
37
  experiment?: Experiment,
37
38
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
39
+ userAgent?: string,
38
40
  |};
39
41
 
40
42
  function isFundingVaultable({
@@ -84,6 +86,7 @@ export function isFundingEligible(
84
86
  supportedNativeBrowser,
85
87
  experiment,
86
88
  displayOnly,
89
+ userAgent,
87
90
  }: IsFundingEligibleOptions
88
91
  ): boolean {
89
92
  if (!fundingEligibility[source] || !fundingEligibility[source].eligible) {
@@ -156,10 +159,17 @@ export function isFundingEligible(
156
159
  return false;
157
160
  }
158
161
 
159
- if (fundingConfig.requires) {
162
+ if (fundingConfig.requires && userAgent) {
160
163
  const required = fundingConfig.requires({ experiment, platform });
161
-
162
- if (required.popup === true && supportsPopups === false) {
164
+ const popupSupport =
165
+ source === FUNDING.VENMO
166
+ ? supportsVenmoPopups(experiment, supportsPopups, userAgent)
167
+ : supportsPopups;
168
+ const nativeBrowserSupport =
169
+ source === FUNDING.VENMO
170
+ ? isSupportedNativeVenmoBrowser(experiment, userAgent)
171
+ : supportedNativeBrowser;
172
+ if (required.popup === true && popupSupport === false) {
163
173
  return false;
164
174
  }
165
175
 
@@ -167,7 +177,7 @@ export function isFundingEligible(
167
177
  return false;
168
178
  }
169
179
 
170
- if (required.native === true && supportedNativeBrowser === false) {
180
+ if (required.native === true && nativeBrowserSupport === false) {
171
181
  return false;
172
182
  }
173
183
  }
@@ -204,6 +214,7 @@ export function determineEligibleFunding({
204
214
  supportedNativeBrowser,
205
215
  experiment,
206
216
  displayOnly = [],
217
+ userAgent = "",
207
218
  }: {|
208
219
  fundingSource: ?$Values<typeof FUNDING>,
209
220
  remembered: $ReadOnlyArray<$Values<typeof FUNDING>>,
@@ -223,6 +234,7 @@ export function determineEligibleFunding({
223
234
  supportedNativeBrowser: boolean,
224
235
  experiment: Experiment,
225
236
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
237
+ userAgent?: string,
226
238
  |}): $ReadOnlyArray<$Values<typeof FUNDING>> {
227
239
  if (fundingSource) {
228
240
  return [fundingSource];
@@ -247,6 +259,7 @@ export function determineEligibleFunding({
247
259
  supportedNativeBrowser,
248
260
  experiment,
249
261
  displayOnly,
262
+ userAgent,
250
263
  })
251
264
  );
252
265
 
@@ -1,10 +1,23 @@
1
1
  /* @flow */
2
- import { COMPONENTS, FUNDING, PLATFORM } from "@paypal/sdk-constants/src";
3
- import { describe, expect } from "vitest";
2
+ import { COMPONENTS, FUNDING } from "@paypal/sdk-constants/src";
3
+ import { describe, expect, vi, beforeEach, afterEach } from "vitest";
4
4
 
5
5
  import { BUTTON_FLOW } from "../constants";
6
6
 
7
7
  import { isFundingEligible, isWalletFundingEligible } from "./funding";
8
+ import { supportsVenmoPopups, isSupportedNativeVenmoBrowser } from "./util";
9
+ import { getFundingConfig } from "./config";
10
+
11
+ // Mock the venmo utility functions
12
+ vi.mock("./util", () => ({
13
+ supportsVenmoPopups: vi.fn(),
14
+ isSupportedNativeVenmoBrowser: vi.fn(),
15
+ }));
16
+
17
+ // Mock getFundingConfig to control funding config behavior
18
+ vi.mock("./config", () => ({
19
+ getFundingConfig: vi.fn(),
20
+ }));
8
21
 
9
22
  const defaultMockFundingOptions = {
10
23
  platform: "desktop",
@@ -55,9 +68,41 @@ const defaultMockFundingOptions = {
55
68
  onShippingChange: null,
56
69
  onShippingAddressChange: null,
57
70
  onShippingOptionsChange: null,
71
+ userAgent:
72
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
58
73
  };
59
74
 
60
75
  describe("Funding eligibility", () => {
76
+ beforeEach(() => {
77
+ // Mock getFundingConfig with basic configs for all funding sources
78
+ vi.mocked(getFundingConfig).mockReturnValue({
79
+ [FUNDING.PAYLATER]: {
80
+ enabled: true,
81
+ automatic: true,
82
+ },
83
+ [FUNDING.CARD]: {
84
+ enabled: true,
85
+ automatic: true,
86
+ },
87
+ [FUNDING.SEPA]: {
88
+ enabled: false,
89
+ automatic: false,
90
+ },
91
+ [FUNDING.OXXO]: {
92
+ enabled: false,
93
+ automatic: false,
94
+ },
95
+ [FUNDING.VENMO]: {
96
+ enabled: true,
97
+ automatic: true,
98
+ },
99
+ [FUNDING.PAYPAL]: {
100
+ enabled: true,
101
+ automatic: true,
102
+ },
103
+ });
104
+ });
105
+
61
106
  describe("Desktop", () => {
62
107
  test("should not be eligible if funding source is missing from fundingEligibility", () => {
63
108
  const fundingEligible = isFundingEligible(
@@ -173,29 +218,272 @@ describe("Funding eligibility", () => {
173
218
  });
174
219
  });
175
220
 
176
- describe("Mobile", () => {
177
- test("should be eligible if window.popupBridge is defined for Venmo and supportsPopups is false", () => {
178
- window.popupBridge = {};
221
+ describe("Venmo-specific funding requirements", () => {
222
+ beforeEach(() => {
223
+ // Reset all mocks before each test
224
+ vi.clearAllMocks();
179
225
 
180
- const fundingEligible = isFundingEligible(FUNDING.VENMO, {
181
- ...defaultMockFundingOptions,
182
- platform: PLATFORM.MOBILE,
183
- supportsPopups: false,
226
+ // Mock getFundingConfig to return configs with venmo requirements
227
+ vi.mocked(getFundingConfig).mockReturnValue({
228
+ [FUNDING.PAYLATER]: {
229
+ enabled: true,
230
+ automatic: true,
231
+ },
232
+ [FUNDING.CARD]: {
233
+ enabled: true,
234
+ automatic: true,
235
+ },
236
+ [FUNDING.SEPA]: {
237
+ enabled: false,
238
+ automatic: false,
239
+ },
240
+ [FUNDING.OXXO]: {
241
+ enabled: false,
242
+ automatic: false,
243
+ },
244
+ [FUNDING.VENMO]: {
245
+ enabled: true,
246
+ automatic: true,
247
+ requires: () => ({
248
+ popup: true,
249
+ native: true,
250
+ }),
251
+ },
252
+ [FUNDING.PAYPAL]: {
253
+ enabled: true,
254
+ automatic: true,
255
+ requires: () => ({
256
+ popup: true,
257
+ native: true,
258
+ }),
259
+ },
184
260
  });
261
+ });
262
+
263
+ afterEach(() => {
264
+ vi.resetAllMocks();
265
+ });
185
266
 
186
- expect(fundingEligible).toBe(true);
267
+ test("should use supportsVenmoPopups for venmo funding source when popup is required", () => {
268
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
269
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
187
270
 
188
- window.popupBridge = undefined;
271
+ const options = {
272
+ ...defaultMockFundingOptions,
273
+ fundingSource: FUNDING.VENMO,
274
+ platform: "mobile",
275
+ experiment: { venmoEnableWebOnNonNativeBrowser: true },
276
+ };
277
+
278
+ const result = isFundingEligible(FUNDING.VENMO, options);
279
+
280
+ expect(supportsVenmoPopups).toHaveBeenCalledWith(
281
+ options.experiment,
282
+ true,
283
+ options.userAgent
284
+ );
285
+ expect(result).toBe(true);
286
+ });
287
+
288
+ test("should use isSupportedNativeVenmoBrowser for venmo funding source when native is required", () => {
289
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
290
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
291
+
292
+ const options = {
293
+ ...defaultMockFundingOptions,
294
+ fundingSource: FUNDING.VENMO,
295
+ platform: "mobile",
296
+ experiment: { venmoEnableWebOnNonNativeBrowser: true },
297
+ };
298
+
299
+ const result = isFundingEligible(FUNDING.VENMO, options);
300
+
301
+ expect(isSupportedNativeVenmoBrowser).toHaveBeenCalledWith(
302
+ options.experiment,
303
+ options.userAgent
304
+ );
305
+ expect(result).toBe(true);
306
+ });
307
+
308
+ test("should return false when venmo popup support is required but supportsVenmoPopups returns false", () => {
309
+ vi.mocked(supportsVenmoPopups).mockReturnValue(false);
310
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
311
+
312
+ const options = {
313
+ ...defaultMockFundingOptions,
314
+ fundingSource: FUNDING.VENMO,
315
+ platform: "mobile",
316
+ experiment: {},
317
+ };
318
+
319
+ const result = isFundingEligible(FUNDING.VENMO, options);
320
+
321
+ expect(supportsVenmoPopups).toHaveBeenCalledWith(
322
+ options.experiment,
323
+ true,
324
+ options.userAgent
325
+ );
326
+ expect(result).toBe(false);
189
327
  });
190
328
 
191
- test("should not be eligible if window.popupBridge is undefined for Venmo and supportsPopups is false", () => {
192
- const fundingEligible = isFundingEligible(FUNDING.VENMO, {
329
+ test("should return false when venmo native support is required but isSupportedNativeVenmoBrowser returns false", () => {
330
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
331
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(false);
332
+
333
+ const options = {
193
334
  ...defaultMockFundingOptions,
194
- platform: PLATFORM.MOBILE,
195
- supportsPopups: false,
335
+ fundingSource: FUNDING.VENMO,
336
+ platform: "mobile",
337
+ experiment: {},
338
+ };
339
+
340
+ const result = isFundingEligible(FUNDING.VENMO, options);
341
+
342
+ expect(isSupportedNativeVenmoBrowser).toHaveBeenCalledWith(
343
+ options.experiment,
344
+ options.userAgent
345
+ );
346
+ expect(result).toBe(false);
347
+ });
348
+
349
+ test("should use standard supportsPopups for non-venmo funding sources", () => {
350
+ vi.mocked(supportsVenmoPopups).mockReturnValue(false);
351
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(false);
352
+
353
+ // Update the mock to not require popup and native for PayPal to isolate the test
354
+ vi.mocked(getFundingConfig).mockReturnValue({
355
+ [FUNDING.PAYLATER]: {
356
+ enabled: true,
357
+ automatic: true,
358
+ },
359
+ [FUNDING.CARD]: {
360
+ enabled: true,
361
+ automatic: true,
362
+ },
363
+ [FUNDING.SEPA]: {
364
+ enabled: false,
365
+ automatic: false,
366
+ },
367
+ [FUNDING.OXXO]: {
368
+ enabled: false,
369
+ automatic: false,
370
+ },
371
+ [FUNDING.VENMO]: {
372
+ enabled: true,
373
+ automatic: true,
374
+ requires: () => ({
375
+ popup: true,
376
+ native: true,
377
+ }),
378
+ },
379
+ [FUNDING.PAYPAL]: {
380
+ enabled: true,
381
+ automatic: true,
382
+ // No requires function for PayPal to test standard behavior
383
+ },
196
384
  });
197
385
 
198
- expect(fundingEligible).toBe(false);
386
+ const options = {
387
+ ...defaultMockFundingOptions,
388
+ fundingSource: FUNDING.PAYPAL,
389
+ platform: "mobile",
390
+ supportsPopups: true,
391
+ supportedNativeBrowser: true,
392
+ experiment: {},
393
+ fundingEligibility: {
394
+ ...defaultMockFundingOptions.fundingEligibility,
395
+ paypal: {
396
+ eligible: true,
397
+ vaultable: false,
398
+ branded: false,
399
+ },
400
+ },
401
+ };
402
+
403
+ const result = isFundingEligible(FUNDING.PAYPAL, options);
404
+
405
+ // Venmo functions should not be called for non-venmo sources
406
+ expect(supportsVenmoPopups).not.toHaveBeenCalled();
407
+ expect(isSupportedNativeVenmoBrowser).not.toHaveBeenCalled();
408
+ expect(result).toBe(true);
409
+ });
410
+
411
+ test("should handle undefined experiment parameter for venmo", () => {
412
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
413
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
414
+
415
+ const options = {
416
+ ...defaultMockFundingOptions,
417
+ fundingSource: FUNDING.VENMO,
418
+ platform: "mobile",
419
+ experiment: undefined,
420
+ };
421
+
422
+ const result = isFundingEligible(FUNDING.VENMO, options);
423
+
424
+ expect(supportsVenmoPopups).toHaveBeenCalledWith(
425
+ undefined,
426
+ true,
427
+ options.userAgent
428
+ );
429
+ expect(isSupportedNativeVenmoBrowser).toHaveBeenCalledWith(
430
+ undefined,
431
+ options.userAgent
432
+ );
433
+ expect(result).toBe(true);
434
+ });
435
+
436
+ test("should pass through experiment flags to venmo utility functions", () => {
437
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
438
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
439
+
440
+ const experimentFlags = {
441
+ venmoEnableWebOnNonNativeBrowser: true,
442
+ venmoVaultWithoutPurchase: false,
443
+ };
444
+
445
+ const options = {
446
+ ...defaultMockFundingOptions,
447
+ fundingSource: FUNDING.VENMO,
448
+ platform: "mobile",
449
+ experiment: experimentFlags,
450
+ };
451
+
452
+ const result = isFundingEligible(FUNDING.VENMO, options);
453
+
454
+ expect(supportsVenmoPopups).toHaveBeenCalledWith(
455
+ experimentFlags,
456
+ true,
457
+ options.userAgent
458
+ );
459
+ expect(isSupportedNativeVenmoBrowser).toHaveBeenCalledWith(
460
+ experimentFlags,
461
+ options.userAgent
462
+ );
463
+ expect(result).toBe(true);
464
+ });
465
+
466
+ test("should respect combination of venmo popup and native requirements", () => {
467
+ // Test case where popup succeeds but native fails
468
+ vi.mocked(supportsVenmoPopups).mockReturnValue(true);
469
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(false);
470
+
471
+ const options = {
472
+ ...defaultMockFundingOptions,
473
+ fundingSource: FUNDING.VENMO,
474
+ platform: "mobile",
475
+ experiment: {},
476
+ };
477
+
478
+ const result = isFundingEligible(FUNDING.VENMO, options);
479
+
480
+ expect(result).toBe(false);
481
+
482
+ // Test case where both succeed
483
+ vi.mocked(isSupportedNativeVenmoBrowser).mockReturnValue(true);
484
+
485
+ const result2 = isFundingEligible(FUNDING.VENMO, options);
486
+ expect(result2).toBe(true);
199
487
  });
200
488
  });
201
489
  });
@@ -13,7 +13,7 @@ import {
13
13
  PaylaterMarkRebrandExternalImage,
14
14
  } from "@paypal/sdk-logos/src";
15
15
 
16
- import { Logo } from "../paypal/template";
16
+ import { Logo as PayPalRebrandLogo } from "../paypal/template";
17
17
  import { BUTTON_COLOR, BUTTON_LAYOUT, DEFAULT } from "../../constants";
18
18
  import { DEFAULT_FUNDING_CONFIG, type FundingSourceConfig } from "../common";
19
19
  import { Text } from "../../ui/text";
@@ -111,7 +111,7 @@ export function getPaylaterConfig(): FundingSourceConfig {
111
111
 
112
112
  return (
113
113
  <Style css={css} nonce={nonce}>
114
- <Logo
114
+ <PayPalRebrandLogo
115
115
  logoColor={logoColor}
116
116
  shouldApplyRebrandedStyles={shouldApplyRebrandedStyles}
117
117
  env={env}
@@ -0,0 +1,99 @@
1
+ /* @flow */
2
+
3
+ import {
4
+ isWebView,
5
+ isIosWebview,
6
+ isAndroidWebview,
7
+ isFacebookWebView,
8
+ isOperaMini,
9
+ isFirefoxIOS,
10
+ isEdgeIOS,
11
+ isQQBrowser,
12
+ isElectron,
13
+ isTablet,
14
+ isIos,
15
+ isSafari,
16
+ isAndroid,
17
+ isChrome,
18
+ isFirefox,
19
+ } from "@krakenjs/belter/src";
20
+
21
+ import type { Experiment } from "../types";
22
+
23
+ const isMacOsCna = (userAgent: string): boolean => {
24
+ return /Macintosh.*AppleWebKit(?!.*Safari)/i.test(userAgent);
25
+ };
26
+
27
+ const isVenmoSupportedWebView = (userAgent: string): boolean => {
28
+ return (
29
+ isWebView(userAgent) ||
30
+ isIosWebview(userAgent) ||
31
+ isAndroidWebview(userAgent) ||
32
+ isFacebookWebView(userAgent)
33
+ );
34
+ };
35
+
36
+ const venmoUserAgentSupportsPopups = (userAgent: string): boolean => {
37
+ return !(
38
+ isVenmoSupportedWebView(userAgent) ||
39
+ isOperaMini(userAgent) ||
40
+ isFirefoxIOS(userAgent) ||
41
+ isEdgeIOS(userAgent) ||
42
+ isQQBrowser(userAgent) ||
43
+ isMacOsCna(userAgent) ||
44
+ isElectron()
45
+ );
46
+ };
47
+
48
+ export function supportsVenmoPopups(
49
+ experiment?: Experiment,
50
+ supportsPopups: boolean,
51
+ userAgent: string
52
+ ): boolean {
53
+ if (isVenmoSupportedWebView(userAgent)) {
54
+ if (typeof window !== "undefined" && window.popupBridge) {
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ if (experiment?.venmoEnableWebOnNonNativeBrowser === true) {
61
+ return venmoUserAgentSupportsPopups(userAgent);
62
+ }
63
+ return supportsPopups;
64
+ }
65
+
66
+ export function isSupportedNativeVenmoBrowser(
67
+ experiment?: Experiment,
68
+ userAgent: string
69
+ ): boolean {
70
+ if (isVenmoSupportedWebView(userAgent)) {
71
+ if (typeof window !== "undefined" && window.popupBridge) {
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ if (isTablet(userAgent)) {
78
+ return false;
79
+ }
80
+
81
+ // Default supported browsers for Venmo
82
+ if (
83
+ (isIos(userAgent) && isSafari(userAgent)) ||
84
+ (isAndroid(userAgent) && isChrome(userAgent))
85
+ ) {
86
+ return true;
87
+ }
88
+
89
+ // Additional browsers enabled by experiment
90
+ if (
91
+ experiment?.venmoEnableWebOnNonNativeBrowser === true &&
92
+ ((isIos(userAgent) && isChrome(userAgent)) ||
93
+ (isAndroid(userAgent) && isFirefox(userAgent)))
94
+ ) {
95
+ return true;
96
+ }
97
+
98
+ return false;
99
+ }