From 57e544885c41022484db31a331abb659ef2c880c Mon Sep 17 00:00:00 2001 From: Cole Denslow Date: Wed, 3 Apr 2024 16:49:27 -0500 Subject: [PATCH] [INTER-2970] Fastlane implementation --- package.json | 2 + src/braintree/getBraintreeJsUrls.ts | 18 +- src/fastlane/index.ts | 2 + src/fastlane/initFastlane.ts | 95 +++++++ src/fastlane/manageFastlaneState.ts | 17 ++ src/index.ts | 1 + src/initialize/initialize.ts | 6 +- src/types/braintree.ts | 38 ++- src/types/errors.ts | 6 + src/types/fastlane.ts | 98 +++++++ src/types/index.ts | 1 + src/types/props.ts | 2 +- src/types/variables.ts | 2 + src/variables/variables.ts | 8 +- .../apple/braintreeOnLoadApple.test.ts | 2 + tests/braintree/getBraintreeJsUrls.test.ts | 20 +- .../google/braintreeOnloadGoogle.test.ts | 2 + .../google/initBraintreeGoogle.test.ts | 3 +- tests/braintree/initBraintreeApple.test.ts | 3 +- tests/braintree/manageBraintreeState.test.ts | 6 +- tests/fastlane/initFastlane.test.ts | 242 ++++++++++++++++++ tests/fastlane/manageFastlaneState.test.ts | 51 ++++ yarn.lock | 103 +++++++- 23 files changed, 708 insertions(+), 20 deletions(-) create mode 100644 src/fastlane/index.ts create mode 100644 src/fastlane/initFastlane.ts create mode 100644 src/fastlane/manageFastlaneState.ts create mode 100644 src/types/fastlane.ts create mode 100644 tests/fastlane/initFastlane.test.ts create mode 100644 tests/fastlane/manageFastlaneState.test.ts diff --git a/package.json b/package.json index d335586..51a353b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "author": "", "devDependencies": { "@types/jest": "^28.1.2", + "@types/node": "^20.12.6", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", @@ -36,6 +37,7 @@ "jest-junit": "^14.0.0", "lint-staged": "^11.1.2", "ts-jest": "^28.0.5", + "ts-node": "^10.9.2", "ttypescript": "^1.5.12", "typedoc": "^0.22.18", "typedoc-plugin-markdown": "^3.13.6", diff --git a/src/braintree/getBraintreeJsUrls.ts b/src/braintree/getBraintreeJsUrls.ts index 4d37cca..db3e9d6 100644 --- a/src/braintree/getBraintreeJsUrls.ts +++ b/src/braintree/getBraintreeJsUrls.ts @@ -1,19 +1,25 @@ import {braintreeConstants, IBraintreeUrls} from 'src'; -export function getBraintreeJsUrls(): IBraintreeUrls { +/** + * @param version If provided, URLs will be built with this version instead + */ +export function getBraintreeJsUrls(version?: string): IBraintreeUrls { const { BASE_JS_URL: base, APPLE_JS: appleJs, GOOGLE_JS: googleJs, CLIENT_JS: clientJs, + FASTLANE_JS: fastlaneJs, DATA_COLLECTOR_JS: dataCollectorJs, GOOGLE_JS_URL: googleJsUrl, JS_VERSION: jsVersion } = braintreeConstants; - const clientJsURL = `${base}/${jsVersion}/${clientJs}`; - const appleJsURL = `${base}/${jsVersion}/${appleJs}`; - const braintreeGoogleJsURL = `${base}/${jsVersion}/${googleJs}`; - const dataCollectorJsURL = `${base}/${jsVersion}/${dataCollectorJs}`; + version ??= jsVersion; + const clientJsURL = `${base}/${version}/${clientJs}`; + const appleJsURL = `${base}/${version}/${appleJs}`; + const braintreeGoogleJsURL = `${base}/${version}/${googleJs}`; + const dataCollectorJsURL = `${base}/${version}/${dataCollectorJs}`; + const fastlaneJsURL = `${base}/${version}/${fastlaneJs}`; - return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL}; + return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL, fastlaneJsURL}; } diff --git a/src/fastlane/index.ts b/src/fastlane/index.ts new file mode 100644 index 0000000..cea6568 --- /dev/null +++ b/src/fastlane/index.ts @@ -0,0 +1,2 @@ +export * from './initFastlane'; +export * from './manageFastlaneState'; \ No newline at end of file diff --git a/src/fastlane/initFastlane.ts b/src/fastlane/initFastlane.ts new file mode 100644 index 0000000..40135fa --- /dev/null +++ b/src/fastlane/initFastlane.ts @@ -0,0 +1,95 @@ +import { getPublicOrderId, getEnvironment, getShopIdentifier, getJwtToken } from '@boldcommerce/checkout-frontend-library'; +import { loadScript } from '@paypal/paypal-js'; +import { + loadJS, + getBraintreeJsUrls, + braintreeOnLoadClient, + IFastlaneInstance, + getBraintreeClient, + IBraintreeClient, + FastlaneLoadingError, +} from 'src'; + +interface TokenResponse { + is_test_mode: boolean; + client_token: string; +} + +interface BraintreeTokenResponse extends TokenResponse { + type: 'braintree'; + client_id: null; +} + +interface PPCPTokenResponse extends TokenResponse { + type: 'ppcp'; + client_id: string; +} + +export async function initFastlane(): Promise { + const {clientJsURL, dataCollectorJsURL, fastlaneJsURL} = getBraintreeJsUrls('3.101.0-fastlane-beta.7.2'); + + try { + // TODO move this request to the checkout frontend library + const env = getEnvironment(); + const shopId = getShopIdentifier(); + const publicOrderId = getPublicOrderId(); + const jwt = getJwtToken(); + const resp = await fetch(`${env.url}/checkout/storefront/${shopId}/${publicOrderId}/paypal_fastlane/client_token`, { + headers: { + Authorization: `Bearer ${jwt}`, + }, + }); + + // Getting client token and which SDK to use + const { + client_token: clientToken, + client_id: clientId, + type, + is_test_mode: isTest, + } = await resp.json().then(r => r.data) as BraintreeTokenResponse | PPCPTokenResponse; + + switch (type) { + case 'braintree': { + await Promise.all([ + loadJS(clientJsURL), + loadJS(fastlaneJsURL), + loadJS(dataCollectorJsURL), + ]).then(braintreeOnLoadClient); + + const braintree = getBraintreeClient() as IBraintreeClient; + const client = await braintree.client.create({authorization: clientToken}); + const dataCollector = await braintree.dataCollector.create({ + client: client, + riskCorrelationId: getPublicOrderId(), + }); + const fastlane = await braintree.fastlane.create({ + client, + authorization: clientToken, + deviceData: dataCollector.deviceData, + }); + + return fastlane; + } + case 'ppcp': { + const paypal = await loadScript({ + dataUserIdToken: clientToken, + clientId: clientId, + components: 'fastlane', + debug: isTest, + }) as unknown as {Fastlane: () => Promise}; + const fastlane = await paypal.Fastlane(); + + return fastlane; + } + default: + throw new Error(`unknown type: ${type}`); + } + } catch (error) { + if (error instanceof Error) { + error.name = FastlaneLoadingError.name; + throw error; + } + + throw new FastlaneLoadingError(`Error loading Fastlane: ${error}`); + } +} diff --git a/src/fastlane/manageFastlaneState.ts b/src/fastlane/manageFastlaneState.ts new file mode 100644 index 0000000..fdf4ea0 --- /dev/null +++ b/src/fastlane/manageFastlaneState.ts @@ -0,0 +1,17 @@ +import {IFastlaneInstance} from 'src/types'; +import {fastlaneState} from 'src/variables'; +import {initFastlane} from './initFastlane'; + +/** + * Gets an instance of Fastlane. If the instance has not yet been initialized then + * one will be initialized and returned asynchronously. Calls to `getFastlaneInstance` while + * and instance is being initialized will return the same promise, avoiding duplicate initializations + * of the Fastlane instance. + */ +export const getFastlaneInstance = async (): Promise => { + return fastlaneState.instance ?? (fastlaneState.instance = initFastlane().catch((e) => { + // Clearing the rejected promise from state so we can try again + fastlaneState.instance = null; + throw e; + })); +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index bb47d22..f31a52e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export * from './types'; export * from './utils'; export * from './variables'; export * from './utils'; +export * from './fastlane'; \ No newline at end of file diff --git a/src/initialize/initialize.ts b/src/initialize/initialize.ts index d9a4917..028967c 100644 --- a/src/initialize/initialize.ts +++ b/src/initialize/initialize.ts @@ -21,8 +21,8 @@ export function initialize(props: IInitializeProps): void{ const {alternative_payment_methods} = getOrderInitialData(); setOnAction(props.onAction); - if(alternative_payment_methods){ - alternative_payment_methods.forEach(paymentMethod => { + if (alternative_payment_methods){ + for (const paymentMethod of alternative_payment_methods) { const type = paymentMethod.type; switch (type){ case alternatePaymentMethodType.STRIPE: @@ -45,6 +45,6 @@ export function initialize(props: IInitializeProps): void{ console.log('do nothing'); // TODO Implement the default behaviour. break; } - }); + } } } diff --git a/src/types/braintree.ts b/src/types/braintree.ts index 04cb2a3..6e2a584 100644 --- a/src/types/braintree.ts +++ b/src/types/braintree.ts @@ -1,3 +1,4 @@ +import {IFastlaneInstance} from './fastlane'; import ApplePayPaymentRequest = ApplePayJS.ApplePayPaymentRequest; import ApplePayPaymentToken = ApplePayJS.ApplePayPaymentToken; import GooglePaymentData = google.payments.api.PaymentData; @@ -10,12 +11,35 @@ export interface IBraintreeClient { }; applePay: { create: IBraintreeApplePayCreate - } + }; googlePayment: { create: IBraintreeGooglePayCreate - } + }; + dataCollector: { + create: (_: { + client: IBraintreeClientInstance; + riskCorrelationId?: string; + }) => Promise; + }; + fastlane: { + create: IBraintreeFastlaneCreate; + }; } +export interface IBraintreeFastlaneCreateRequest { + authorization: string; + client: IBraintreeClientInstance; + deviceData: unknown; + metadata?: { + geoLocOverride: string; + }; +} + +export interface IBraintreeDataCollectorCreateRequest { + client: IBraintreeClientInstance; + riskCorrelationId?: string; +} + export interface IBraintreeClientCreateRequest { authorization: string; } @@ -81,6 +105,10 @@ export interface IBraintreeApplePayPaymentAuthorizedResponse { } } +export interface IBraintreeDataCollectorInstance { + deviceData: unknown; +} + export type IBraintreeRequiredContactField = Array<'postalAddress' | 'email' | 'phone'>; export type IBraintreeClientInstance = Record; export type IBraintreeClientCreateCallback = (error: string | Error | undefined, instance: IBraintreeClientInstance) => void; @@ -88,6 +116,8 @@ export type IBraintreeApplePayCreateCallback = (error: string | Error | undefine export type IBraintreeGooglePayCreateCallback = (error: string | Error | undefined, instance: IBraintreeGooglePayInstance) => void; export type IBraintreeApplePayPerformValidationCallback = (error: string | Error | undefined, merchantSession: unknown) => void; export type IBraintreeApplePayPaymentAuthorizedCallback = (error: string | Error | undefined, payload: IBraintreeApplePayPaymentAuthorizedResponse | undefined) => void; -export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => IBraintreeClientInstance; +export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => Promise; export type IBraintreeApplePayCreate = (request: IBraintreeApplePayCreateRequest, callback?: IBraintreeApplePayCreateCallback) => IBraintreeApplePayInstance; -export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => IBraintreeGooglePayInstance; +export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => Promise; +export type IBraintreeFastlaneCreate = (request: IBraintreeFastlaneCreateRequest) => Promise; +export type IBraintreeDataCollectorCreate = (request: IBraintreeDataCollectorCreateRequest) => Promise; \ No newline at end of file diff --git a/src/types/errors.ts b/src/types/errors.ts index 503b12d..b8041a5 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -14,6 +14,12 @@ export class GooglePayLoadingError extends Error { } } +export class FastlaneLoadingError extends Error { + constructor(message: string) { + super(message); + } +} + export class ApplePayValidateMerchantError extends Error { constructor(message: string) { super(message); diff --git a/src/types/fastlane.ts b/src/types/fastlane.ts new file mode 100644 index 0000000..0a94497 --- /dev/null +++ b/src/types/fastlane.ts @@ -0,0 +1,98 @@ +interface IFastlaneAddress { + firstName: string; + lastName: string; + company?: string; + streetAddress: string; + extendedAddress?: string; + locality: string; // City + region: string; // State + postalCode: string; + countryCodeNumeric?: number; + countryCodeAlpha2: string; + countryCodeAlpha3?: string; + phoneNumber: string; +} + +export interface IFastlanePaymentToken { + id: string; + paymentSource: { + card: { + brand: string; + expiry: string; // "YYYY-MM" + lastDigits: string; // "1111" + name: string; + billingAddress: IFastlaneAddress; + } + } +} + +export interface IFastlanePaymentComponent { + render: (container: string) => IFastlanePaymentComponent; + getPaymentToken: () => Promise; + setShippingAddress: (shippingAddress: IFastlaneAddress) => void; +} + +export interface IFastlaneCardComponent { + render: (container: string) => IFastlaneCardComponent; + getPaymentToken: (options: { + billingAddress: IFastlaneAddress; + }) => Promise; +} + +interface Field { + placeholder?: string; + prefill?: string; +} + +export interface IFastlaneComponentOptions { + styles?: unknown; + fields?: { + number?: Field; + expirationDate?: Field; + expirationMonth?: Field; + expirationYear?: Field + cvv?: Field; + postalCode?: Field; + cardholderName?: Field; + phoneNumber?: Field; + }; + shippingAddress?: IFastlaneAddress; +} + +export interface IFastlaneAuthenticatedCustomerResult { + authenticationState: 'succeeded'|'failed'|'canceled'|'not_found'; + profileData: { + name: { + firstName: string; + lastName: string; + }; + shippingAddress: IFastlaneAddress; + card: IFastlanePaymentToken; + } +} + +export interface IFastlaneInstance { + profile: { + showShippingAddressSelector: () => Promise<{ + selectionChanged: true; + selectedAddress: IFastlaneAddress; + } | { + selectionChanged: false; + selectedAddress: null; + }>; + showCardSelector: () => Promise<{ + selectionChanged: true; + selectedCard: IFastlanePaymentToken; + } | { + selectionChanged: false; + selectedCard: null; + }>; + }; + setLocale: (locale: string) => void; + identity: { + lookupCustomerByEmail: (email: string) => Promise<{customerContextId: string}>; + triggerAuthenticationFlow: (customerContextId: string) => Promise + }; + FastlanePaymentComponent: (options: IFastlaneComponentOptions) => Promise; + FastlaneCardComponent: (options: Omit) => IFastlaneCardComponent; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 4d4124c..64b1331 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,3 +5,4 @@ export * from './paypal'; export * from './props'; export * from './stripeProps'; export * from './variables'; +export * from './fastlane'; \ No newline at end of file diff --git a/src/types/props.ts b/src/types/props.ts index 36dc0ec..26cc81f 100644 --- a/src/types/props.ts +++ b/src/types/props.ts @@ -8,7 +8,7 @@ declare global { } export interface IInitializeProps { - onAction: IOnAction + onAction: IOnAction; } export interface IGetFirstAndLastName { diff --git a/src/types/variables.ts b/src/types/variables.ts index ccf98cb..2db1bec 100644 --- a/src/types/variables.ts +++ b/src/types/variables.ts @@ -82,6 +82,7 @@ export interface IBraintreeConstants { BASE_JS_URL: string; GOOGLE_JS_URL: string; CLIENT_JS: string; + FASTLANE_JS: string; APPLE_JS: string; GOOGLE_JS: string; DATA_COLLECTOR_JS: string; @@ -114,6 +115,7 @@ export interface IBraintreeUrls { dataCollectorJsURL: string; googleJsUrl: string; braintreeGoogleJsURL: string; + fastlaneJsURL: string; } export interface ITotals { diff --git a/src/variables/variables.ts b/src/variables/variables.ts index 9a7b395..d240dd5 100644 --- a/src/variables/variables.ts +++ b/src/variables/variables.ts @@ -1,3 +1,4 @@ +import { IFastlaneInstance } from 'src/types'; import { IActionTypes, IApplePayConstants, @@ -63,12 +64,17 @@ export const braintreeState: IBraintreeState = { appleInstance: null, appleSession: null, googleCredentials: null, - appleCredentials: null + appleCredentials: null, +}; + +export const fastlaneState = { + instance: null as null | Promise, }; export const braintreeConstants: IBraintreeConstants = { BASE_JS_URL: 'https://js.braintreegateway.com/web', GOOGLE_JS_URL: 'https://pay.google.com/gp/p/js/pay.js', + FASTLANE_JS: 'js/fastlane.min.js', CLIENT_JS: 'js/client.min.js', APPLE_JS: 'js/apple-pay.min.js', GOOGLE_JS: 'js/google-payment.min.js', diff --git a/tests/braintree/apple/braintreeOnLoadApple.test.ts b/tests/braintree/apple/braintreeOnLoadApple.test.ts index 6b0c8fb..aabc765 100644 --- a/tests/braintree/apple/braintreeOnLoadApple.test.ts +++ b/tests/braintree/apple/braintreeOnLoadApple.test.ts @@ -30,6 +30,8 @@ describe('testing braintreeOnLoadGoogle function',() => { client: {create: clientCreate}, googlePayment: {create: googlePayCreate}, applePay: {create: applePayCreate}, + dataCollector: {create: jest.fn()}, + fastlane: {create: jest.fn()}, } as IBraintreeClient; const createdClient = {} as IBraintreeClientInstance; const createdAppleInstance = {} as IBraintreeApplePayInstance; diff --git a/tests/braintree/getBraintreeJsUrls.test.ts b/tests/braintree/getBraintreeJsUrls.test.ts index 14c1dc9..3e4539a 100644 --- a/tests/braintree/getBraintreeJsUrls.test.ts +++ b/tests/braintree/getBraintreeJsUrls.test.ts @@ -8,7 +8,8 @@ describe('testing getBraintreeJsUrls function', () => { googleJsUrl: 'https://pay.google.com/gp/p/js/pay.js', dataCollectorJsURL: 'https://js.braintreegateway.com/web/3.88.2/js/data-collector.min.js', appleJsURL: 'https://js.braintreegateway.com/web/3.88.2/js/apple-pay.min.js', - braintreeGoogleJsURL: 'https://js.braintreegateway.com/web/3.88.2/js/google-payment.min.js' + braintreeGoogleJsURL: 'https://js.braintreegateway.com/web/3.88.2/js/google-payment.min.js', + fastlaneJsURL: 'https://js.braintreegateway.com/web/3.88.2/js/fastlane.min.js' }; const result = getBraintreeJsUrls(); @@ -16,4 +17,21 @@ describe('testing getBraintreeJsUrls function', () => { expect(result).toStrictEqual(expected); }); + test('is getting the correct Urls with specified version', () => { + // Arranging + const expected: IBraintreeUrls = { + clientJsURL: 'https://js.braintreegateway.com/web/testing/js/client.min.js', + googleJsUrl: 'https://pay.google.com/gp/p/js/pay.js', + dataCollectorJsURL: 'https://js.braintreegateway.com/web/testing/js/data-collector.min.js', + appleJsURL: 'https://js.braintreegateway.com/web/testing/js/apple-pay.min.js', + braintreeGoogleJsURL: 'https://js.braintreegateway.com/web/testing/js/google-payment.min.js', + fastlaneJsURL: 'https://js.braintreegateway.com/web/testing/js/fastlane.min.js' + }; + + // Acting + const result = getBraintreeJsUrls('testing'); + + // Asserting + expect(result).toStrictEqual(expected); + }); }); diff --git a/tests/braintree/google/braintreeOnloadGoogle.test.ts b/tests/braintree/google/braintreeOnloadGoogle.test.ts index f29d1e8..5d1b942 100644 --- a/tests/braintree/google/braintreeOnloadGoogle.test.ts +++ b/tests/braintree/google/braintreeOnloadGoogle.test.ts @@ -49,6 +49,8 @@ describe('testing braintreeOnLoadGoogle function',() => { client: {create: jest.fn()}, googlePayment: {create: jest.fn()}, applePay: {create: jest.fn()}, + fastlane: {create: jest.fn()}, + dataCollector: {create: jest.fn()}, } as IBraintreeClient); hasBraintreeMock.mockReturnValueOnce(true); await braintreeOnLoadGoogle().then(() => { diff --git a/tests/braintree/google/initBraintreeGoogle.test.ts b/tests/braintree/google/initBraintreeGoogle.test.ts index 423f2a7..0d5043b 100644 --- a/tests/braintree/google/initBraintreeGoogle.test.ts +++ b/tests/braintree/google/initBraintreeGoogle.test.ts @@ -46,7 +46,8 @@ const jsUrls: IBraintreeUrls = { googleJsUrl: 'https://test.com/googleJsUrl.js', dataCollectorJsURL: 'https://test.com/dataCollectorJsURL.js', appleJsURL: 'https://test.com/appleJsURL.js', - braintreeGoogleJsURL: 'https://test.com/braintreeGoogleJsURL.js' + braintreeGoogleJsURL: 'https://test.com/braintreeGoogleJsURL.js', + fastlaneJsURL: 'https://test.com/fastlaneURL.js' }; describe('testing initBraintreeGoogle function', () => { diff --git a/tests/braintree/initBraintreeApple.test.ts b/tests/braintree/initBraintreeApple.test.ts index 672a6cc..6f758fa 100644 --- a/tests/braintree/initBraintreeApple.test.ts +++ b/tests/braintree/initBraintreeApple.test.ts @@ -41,7 +41,8 @@ const jsUrls: IBraintreeUrls = { googleJsUrl: 'https://test.com/googleJsUrl.js', dataCollectorJsURL: 'https://test.com/dataCollectorJsURL.js', appleJsURL: 'https://test.com/appleJsURL.js', - braintreeGoogleJsURL: 'https://test.com/braintreeGoogleJsURL.js' + braintreeGoogleJsURL: 'https://test.com/braintreeGoogleJsURL.js', + fastlaneJsURL: 'https://test.com/fastlaneURL.js' }; const supportsVersionMock = jest.fn(); const canMakePaymentsMock = jest.fn(); diff --git a/tests/braintree/manageBraintreeState.test.ts b/tests/braintree/manageBraintreeState.test.ts index b59e3c9..af7423e 100644 --- a/tests/braintree/manageBraintreeState.test.ts +++ b/tests/braintree/manageBraintreeState.test.ts @@ -62,7 +62,11 @@ const googlePayClient: PaymentsClient = { prefetchPaymentData: jest.fn(), }; const braintree: IBraintreeClient = { - applePay: {create: jest.fn()}, client: {create: jest.fn()}, googlePayment: {create: jest.fn()} + applePay: {create: jest.fn()}, + client: {create: jest.fn()}, + googlePayment: {create: jest.fn()}, + dataCollector: {create: jest.fn()}, + fastlane: {create: jest.fn()}, }; describe('testing manageBraintreeState functions', () => { diff --git a/tests/fastlane/initFastlane.test.ts b/tests/fastlane/initFastlane.test.ts new file mode 100644 index 0000000..fc201e0 --- /dev/null +++ b/tests/fastlane/initFastlane.test.ts @@ -0,0 +1,242 @@ +import {getEnvironment, getJwtToken, getPublicOrderId, getShopIdentifier} from '@boldcommerce/checkout-frontend-library'; +import {loadScript} from '@paypal/paypal-js'; +import {mocked} from 'jest-mock'; +import {IBraintreeUrls, braintreeOnLoadClient, getBraintreeClient, getBraintreeJsUrls, initFastlane, loadJS} from 'src'; + +jest.mock('src/braintree/getBraintreeJsUrls.ts'); +jest.mock('src/utils/loadJS.ts'); +jest.mock('src/braintree/manageBraintreeState.ts'); +jest.mock('src/braintree/braintreeOnLoadClient.ts'); +jest.mock('src/fastlane/manageFastlaneState.ts'); +jest.mock('@boldcommerce/checkout-frontend-library', () => ({ + ...jest.requireActual('@boldcommerce/checkout-frontend-library'), + getEnvironment: jest.fn(), + getJwtToken: jest.fn(), + getPublicOrderId: jest.fn(), + getShopIdentifier: jest.fn(), +})); +jest.mock('@paypal/paypal-js', () => ({ + ...jest.requireActual('@paypal/paypal-js'), + loadScript: jest.fn(), +})); + +const getBraintreeJsUrlsMock = mocked(getBraintreeJsUrls); +const getPublicOrderIdMock = mocked(getPublicOrderId); +const getEnvironmentMock = mocked(getEnvironment); +const getShopIdentifierMock = mocked(getShopIdentifier); +const getJwtTokenMock = mocked(getJwtToken); +const loadJSMock = mocked(loadJS); +const braintreeOnLoadClientMock = mocked(braintreeOnLoadClient); +const getBraintreeClientMock = mocked(getBraintreeClient); +const loadScriptMock = mocked(loadScript) as jest.Mock; + +describe('testing initFastlane function', () => { + let fetchMock: jest.Mock; + let actualFetch: typeof fetch; + + beforeEach(() => { + actualFetch = global.fetch; + fetchMock = global.fetch = jest.fn(); + }); + + afterEach(() => { + global.fetch = actualFetch; + jest.resetAllMocks(); + }); + + test('init braintree correctly', async () => { + // Arranging + getBraintreeJsUrlsMock.mockReturnValue({ + clientJsURL: 'client', dataCollectorJsURL: 'data', fastlaneJsURL: 'fastlane', + } as IBraintreeUrls); + getEnvironmentMock.mockReturnValue({ + path: 'path', + type: 'testing', + url: 'https://staging.com', + }); + getPublicOrderIdMock.mockReturnValue('testOrderId'); + getShopIdentifierMock.mockReturnValue('testShopId'); + getJwtTokenMock.mockReturnValue('jwt'); + fetchMock.mockResolvedValue({ + json: () => Promise.resolve({ + data: { + client_token: 'client_token', + client_id: null, + type: 'braintree', + is_test_mode: false, + }, + }), + }); + const client = {create: jest.fn()}; + const fastlane = {create: jest.fn()}; + fastlane.create.mockReturnValue('testing'); + + const dataCollector = {create: jest.fn()}; + dataCollector.create.mockReturnValue({deviceData: null}); + + getBraintreeClientMock.mockReturnValue({ + client, + fastlane, + dataCollector, + applePay: {create: jest.fn()}, + googlePayment: {create: jest.fn()}, + }); + + // Assigning + const actualFastlane = await initFastlane(); + + // Asserting + expect(actualFastlane).toBe('testing'); + + expect(loadJSMock).toBeCalledTimes(3); + expect(loadJSMock).toBeCalledWith('client'); + expect(loadJSMock).toBeCalledWith('fastlane'); + expect(loadJSMock).toBeCalledWith('data'); + + expect(braintreeOnLoadClientMock).toBeCalled(); + + expect(client.create).toBeCalled(); + expect(dataCollector.create).toBeCalled(); + expect(fastlane.create).toBeCalled(); + + }); + + test('init ppcp correctly', async () => { + // Arranging + getBraintreeJsUrlsMock.mockReturnValue({ + clientJsURL: 'client', dataCollectorJsURL: 'data', fastlaneJsURL: 'fastlane', + } as IBraintreeUrls); + getEnvironmentMock.mockReturnValue({ + path: 'path', + type: 'testing', + url: 'https://staging.com', + }); + getPublicOrderIdMock.mockReturnValue('testOrderId'); + getShopIdentifierMock.mockReturnValue('testShopId'); + getJwtTokenMock.mockReturnValue('jwt'); + fetchMock.mockResolvedValue({ + json: () => Promise.resolve({ + data: { + client_token: 'client_token', + client_id: 'client_id', + type: 'ppcp', + is_test_mode: false, + }, + }), + }); + loadScriptMock.mockResolvedValue({ + Fastlane: () => Promise.resolve('testing'), + }); + + // Assigning + const actualFastlane = await initFastlane(); + + // Asserting + expect(actualFastlane).toBe('testing'); + }); + + test('init error', async () => { + // Arranging + getBraintreeJsUrlsMock.mockReturnValue({ + clientJsURL: 'client', dataCollectorJsURL: 'data', fastlaneJsURL: 'fastlane', + } as IBraintreeUrls); + getEnvironmentMock.mockReturnValue({ + path: 'path', + type: 'testing', + url: 'https://staging.com', + }); + getPublicOrderIdMock.mockReturnValue('testOrderId'); + getShopIdentifierMock.mockReturnValue('testShopId'); + getJwtTokenMock.mockReturnValue('jwt'); + fetchMock.mockResolvedValue({ + json: () => Promise.resolve({ + data: { + client_token: 'client_token', + client_id: 'client_id', + type: 'ppcp', + is_test_mode: false, + }, + }), + }); + loadScriptMock.mockRejectedValue(new Error('Oh no!')); + + // Assigning + try { + await initFastlane(); + } catch (e: unknown) { + // Asserting + expect((e as Error).message).toBe('Oh no!'); + } + }); + + test('init unknown type', async () => { + // Arranging + getBraintreeJsUrlsMock.mockReturnValue({ + clientJsURL: 'client', dataCollectorJsURL: 'data', fastlaneJsURL: 'fastlane', + } as IBraintreeUrls); + getEnvironmentMock.mockReturnValue({ + path: 'path', + type: 'testing', + url: 'https://staging.com', + }); + getPublicOrderIdMock.mockReturnValue('testOrderId'); + getShopIdentifierMock.mockReturnValue('testShopId'); + getJwtTokenMock.mockReturnValue('jwt'); + fetchMock.mockResolvedValue({ + json: () => Promise.resolve({ + data: { + client_token: 'client_token', + client_id: 'client_id', + type: 'unknown', + is_test_mode: false, + }, + }), + }); + + // Assigning + try { + await initFastlane(); + } catch (e: unknown) { + // Asserting + expect((e as Error).message).toContain('unknown type'); + } + }); + + test.each([ + [new Error('test'), 'test'], + ['test', 'Error loading Fastlane: test'], + ])('init exception', async (error, expectedMsg) => { + // Arranging + getBraintreeJsUrlsMock.mockReturnValue({ + clientJsURL: 'client', dataCollectorJsURL: 'data', fastlaneJsURL: 'fastlane', + } as IBraintreeUrls); + getEnvironmentMock.mockReturnValue({ + path: 'path', + type: 'testing', + url: 'https://staging.com', + }); + getPublicOrderIdMock.mockReturnValue('testOrderId'); + getShopIdentifierMock.mockReturnValue('testShopId'); + getJwtTokenMock.mockReturnValue('jwt'); + fetchMock.mockResolvedValue({ + json: () => Promise.resolve({ + data: { + client_token: 'client_token', + client_id: 'client_id', + type: 'ppcp', + is_test_mode: false, + }, + }), + }); + loadScriptMock.mockRejectedValue(error); + + // Assigning + try { + await initFastlane(); + } catch (e: unknown) { + // Asserting + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toBe(expectedMsg); + } + }); +}); \ No newline at end of file diff --git a/tests/fastlane/manageFastlaneState.test.ts b/tests/fastlane/manageFastlaneState.test.ts new file mode 100644 index 0000000..063f829 --- /dev/null +++ b/tests/fastlane/manageFastlaneState.test.ts @@ -0,0 +1,51 @@ +import {IFastlaneInstance, getFastlaneInstance, initFastlane, fastlaneState} from 'src'; +import {mocked} from 'jest-mock'; + +jest.mock('src/fastlane/initFastlane'); +const initFastlaneMock = mocked(initFastlane); + +describe('testing manage fastlane state functions', () => { + afterEach(() => { + jest.resetAllMocks(); + fastlaneState.instance = null; + }); + + test('init fastlane instance should only be called once get instance called multiple times', async () => { + // Arranging + initFastlaneMock.mockResolvedValue({} as IFastlaneInstance); + + // Assigning + await getFastlaneInstance(); + await getFastlaneInstance(); + + // Asserting + expect(initFastlaneMock).toBeCalledTimes(1); + }); + + test('init fastlane instance should be called only once even when init is "in flight"', async () => { + // Arranging + let resolve: (i: IFastlaneInstance) => void; + initFastlaneMock.mockReturnValue(new Promise(r => resolve = r)); + + // Assigning + const calls = [getFastlaneInstance(), getFastlaneInstance()]; + resolve!({} as IFastlaneInstance); + await Promise.all(calls); + + // Asserting + expect(initFastlaneMock).toBeCalledTimes(1); + }); + + test('fastlane state should be cleared when init fastlane errors', async () => { + // Arranging + initFastlaneMock.mockRejectedValueOnce(new Error('oh no!')); + initFastlaneMock.mockResolvedValue({} as IFastlaneInstance); + + // Assigning + await getFastlaneInstance().catch(() => undefined); + await getFastlaneInstance(); + + // Asserting + expect(initFastlaneMock).toBeCalledTimes(2); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d26b4c0..4b1a290 100644 --- a/yarn.lock +++ b/yarn.lock @@ -307,6 +307,13 @@ resolved "https://registry.yarnpkg.com/@boldcommerce/checkout-frontend-library/-/checkout-frontend-library-0.55.0.tgz#5182fcfaca1eed7a84343f57e74cffb8013deba2" integrity sha512-5wpZcaNHRFE6esBW/K58kd72avRjZhbKmjMrgZHxkk/LzqnSA5WJIm3Xp2wG+RaVupck3zNjaF+P+0d81thSRA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -554,6 +561,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -569,6 +581,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.20" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" @@ -629,6 +649,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/applepayjs@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/applepayjs/-/applepayjs-3.0.4.tgz#9806a4b3ccd73dcf169c61a34be7a39f91d77540" @@ -727,6 +767,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.12.6": + version "20.12.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.6.tgz#72d068870518d7da1d97b49db401e2d6a1805294" + integrity sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ== + dependencies: + undici-types "~5.26.4" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -857,11 +904,21 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.5.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -946,6 +1003,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1252,6 +1314,11 @@ cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1329,6 +1396,11 @@ diff-sequences@^28.1.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2572,7 +2644,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -3259,6 +3331,25 @@ ts-jest@^28.0.5: semver "7.x" yargs-parser "^21.0.1" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -3378,6 +3469,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.4.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" @@ -3557,6 +3653,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"