Skip to content

Commit

Permalink
Web SDK sending info via searchParams (#1427)
Browse files Browse the repository at this point in the history
* Web SDK sending info via searchParams

* rename checkCrossOriginOpenerPolicyCompatibility

* checkCrossOriginOpenerPolicy returns policy

* sdk-info.ts

* yarn prebuild

* remove LIB prefix

* comments

* dont format yml

* getter return undefined

* error handling + pathname

* test
  • Loading branch information
fan-zhang-sv authored Oct 16, 2024
1 parent d06584e commit 8386209
Show file tree
Hide file tree
Showing 17 changed files with 144 additions and 53 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/versioning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ jobs:

- name: Install NPM dependencies
run: yarn install --immutable
- name: Update version.ts file

- name: Update sdk-info.ts file
run: yarn prebuild

- name: Commit and branch updated version
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"pretest": "node compile-assets.js",
"test": "jest",
"test:coverage": "yarn test:unit && open coverage/lcov-report/index.html",
"prebuild": "rm -rf ./dist && node -p \"'export const LIB_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts",
"prebuild": "rm -rf ./dist && node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';\\nexport const NAME = \\'' + require('./package.json').name + '\\';'\" > src/sdk-info.ts",
"build": "node compile-assets.js && tsc -p ./tsconfig.build.json && tsc-alias && cp -a src/vendor-js dist",
"dev": "yarn build && tsc --watch & nodemon --watch dist --delay 1 --exec tsc-alias",
"typecheck": "tsc --noEmit",
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getCoinbaseInjectedProvider } from ':util/provider';
jest.mock(':core/type/util');
jest.mock(':util/provider');
jest.mock('./CoinbaseWalletProvider');
jest.mock('./util/crossOriginOpenerPolicy');
jest.mock('./util/checkCrossOriginOpenerPolicy');

describe('CoinbaseWalletSDK', () => {
test('@makeWeb3Provider - return Coinbase Injected Provider', () => {
Expand Down
12 changes: 4 additions & 8 deletions packages/wallet-sdk/src/CoinbaseWalletSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { LogoType, walletLogo } from './assets/wallet-logo';
import { CoinbaseWalletProvider } from './CoinbaseWalletProvider';
import { AppMetadata, Preference, ProviderInterface } from './core/provider/interface';
import { LIB_VERSION } from './version';
import { VERSION } from './sdk-info';
import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage';
import { getFavicon } from ':core/type/util';
import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy';
import { checkCrossOriginOpenerPolicy } from ':util/checkCrossOriginOpenerPolicy';
import { getCoinbaseInjectedProvider } from ':util/provider';
import { validatePreferences } from ':util/validatePreferences';

Expand All @@ -29,7 +29,7 @@ export class CoinbaseWalletSDK {
appChainIds: metadata.appChainIds || [],
};
this.storeLatestVersion();
this.checkCrossOriginOpenerPolicy();
void checkCrossOriginOpenerPolicy();
}

public makeWeb3Provider(preference: Preference = { options: 'all' }): ProviderInterface {
Expand All @@ -50,10 +50,6 @@ export class CoinbaseWalletSDK {

private storeLatestVersion() {
const versionStorage = new ScopedLocalStorage('CBWSDK');
versionStorage.setItem('VERSION', LIB_VERSION);
}

private checkCrossOriginOpenerPolicy() {
void checkCrossOriginOpenerPolicy();
versionStorage.setItem('VERSION', VERSION);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppMetadata, Preference } from 'src/index';

import { LIB_VERSION } from '../../version';
import { VERSION } from '../../sdk-info';
import { Message, MessageID } from '../message';
import { Communicator } from './Communicator';
import { CB_KEYS_URL } from ':core/constants';
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('Communicator', () => {
1,
{
data: {
version: LIB_VERSION,
version: VERSION,
metadata: appMetadata,
preference,
},
Expand All @@ -140,7 +140,7 @@ describe('Communicator', () => {
1,
{
data: {
version: LIB_VERSION,
version: VERSION,
metadata: appMetadata,
preference,
},
Expand All @@ -162,7 +162,7 @@ describe('Communicator', () => {
1,
{
data: {
version: LIB_VERSION,
version: VERSION,
metadata: appMetadata,
preference,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/wallet-sdk/src/core/communicator/Communicator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LIB_VERSION } from '../../version';
import { VERSION } from '../../sdk-info';
import { ConfigMessage, Message, MessageID } from '../message';
import { CB_KEYS_URL } from ':core/constants';
import { standardErrors } from ':core/error';
Expand Down Expand Up @@ -109,7 +109,7 @@ export class Communicator {
this.postMessage({
requestId: message.id,
data: {
version: LIB_VERSION,
version: VERSION,
metadata: this.metadata,
preference: this.preference,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/wallet-sdk/src/core/error/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: error should not depend on walletlink. revisit this.
import { VERSION } from '../../sdk-info';
import { isErrorResponse, Web3Response } from '../../sign/walletlink/relay/type/Web3Response';
import { LIB_VERSION } from '../../version';
import { standardErrorCodes } from './constants';
import { serialize } from './utils';

Expand All @@ -15,7 +15,7 @@ export function serializeError(error: unknown) {
});

const docUrl = new URL('https://docs.cloud.coinbase.com/wallet-sdk/docs/errors');
docUrl.searchParams.set('version', LIB_VERSION);
docUrl.searchParams.set('version', VERSION);
docUrl.searchParams.set('code', serialized.code.toString());
docUrl.searchParams.set('message', serialized.message);

Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/createCoinbaseWalletSDK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const options: CreateCoinbaseWalletSDKOptions = {
preference: { options: 'all' },
};

jest.mock('./util/crossOriginOpenerPolicy');
jest.mock('./util/checkCrossOriginOpenerPolicy');

describe('createCoinbaseWalletSDK', () => {
it('should return an object with a getProvider method', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/wallet-sdk/src/createCoinbaseWalletSDK.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createCoinbaseWalletProvider } from './createCoinbaseWalletProvider';
import { LIB_VERSION } from './version';
import { VERSION } from './sdk-info';
import {
AppMetadata,
ConstructorOptions,
Preference,
ProviderInterface,
} from ':core/provider/interface';
import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage';
import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy';
import { checkCrossOriginOpenerPolicy } from ':util/checkCrossOriginOpenerPolicy';
import { validatePreferences } from ':util/validatePreferences';

export type CreateCoinbaseWalletSDKOptions = Partial<AppMetadata> & {
Expand All @@ -25,7 +25,7 @@ const DEFAULT_PREFERENCE: Preference = {
*/
export function createCoinbaseWalletSDK(params: CreateCoinbaseWalletSDKOptions) {
const versionStorage = new ScopedLocalStorage('CBWSDK');
versionStorage.setItem('VERSION', LIB_VERSION);
versionStorage.setItem('VERSION', VERSION);

void checkCrossOriginOpenerPolicy();

Expand Down
2 changes: 2 additions & 0 deletions packages/wallet-sdk/src/sdk-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const VERSION = '4.1.0';
export const NAME = '@coinbase/wallet-sdk';
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { checkCrossOriginOpenerPolicy } from './crossOriginOpenerPolicy';
import {
checkCrossOriginOpenerPolicy,
getCrossOriginOpenerPolicy,
} from './checkCrossOriginOpenerPolicy';

describe('checkCrossOriginOpenerPolicy', () => {
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
});

it('should not run if window is undefined', () => {
it('should return non-browser-env if window is undefined', async () => {
const originalWindow = global.window;
// @ts-expect-error delete window property
delete global.window;

expect(checkCrossOriginOpenerPolicy()).toBeUndefined();
await checkCrossOriginOpenerPolicy();

expect(getCrossOriginOpenerPolicy()).toBe('non-browser-env');

// Restore the original window object
global.window = originalWindow;
});

it('should fetch the current origin', async () => {
it('should fetch the current origin and pathname', async () => {
global.fetch = jest.fn().mockResolvedValue({
headers: {
get: jest.fn().mockReturnValue(null),
},
ok: true,
});

checkCrossOriginOpenerPolicy();

expect(global.fetch).toHaveBeenCalledWith(window.location.origin, {});
expect(global.fetch).toHaveBeenCalledWith(
`${window.location.origin}${window.location.pathname}`,
{ method: 'HEAD' }
);
});

it('should log an error if Cross-Origin-Opener-Policy is same-origin', async () => {
Expand All @@ -35,29 +44,35 @@ describe('checkCrossOriginOpenerPolicy', () => {
headers: {
get: jest.fn().mockReturnValue('same-origin'),
},
ok: true,
});

await checkCrossOriginOpenerPolicy();
const result = getCrossOriginOpenerPolicy();

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
"Coinbase Wallet SDK requires the Cross-Origin-Opener-Policy header to not be set to 'same-origin'."
)
);
expect(result).toBe('same-origin');
consoleErrorSpy.mockRestore();
});

it('should not log an error if Cross-Origin-Opener-Policy is not same-origin', async () => {
it('should return true and not log an error if Cross-Origin-Opener-Policy is not same-origin', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
global.fetch = jest.fn().mockResolvedValue({
headers: {
get: jest.fn().mockReturnValue('unsafe-none'),
},
ok: true,
});

await checkCrossOriginOpenerPolicy();
const result = getCrossOriginOpenerPolicy();

expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(result).toBe('unsafe-none');
consoleErrorSpy.mockRestore();
});
});
62 changes: 62 additions & 0 deletions packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const COOP_ERROR_MESSAGE = `Coinbase Wallet SDK requires the Cross-Origin-Opener-Policy header to not be set to 'same-origin'. This is to ensure that the SDK can communicate with the Coinbase Smart Wallet app.
Please see https://www.smartwallet.dev/guides/tips/popup-tips#cross-origin-opener-policy for more information.`;

/**
* Creates a checker for the Cross-Origin-Opener-Policy (COOP).
*
* @returns An object with methods to get and check the Cross-Origin-Opener-Policy.
*
* @method getCrossOriginOpenerPolicy
* Retrieves current Cross-Origin-Opener-Policy.
* @throws Will throw an error if the policy has not been checked yet.
*
* @method checkCrossOriginOpenerPolicy
* Checks the Cross-Origin-Opener-Policy of the current environment.
* If in a non-browser environment, sets the policy to 'non-browser-env'.
* If in a browser environment, fetches the policy from the current origin.
* Logs an error if the policy is 'same-origin'.
*/
const createCoopChecker = () => {
let crossOriginOpenerPolicy: string | undefined;

return {
getCrossOriginOpenerPolicy: () => {
if (crossOriginOpenerPolicy === undefined) {
return 'undefined';
}

return crossOriginOpenerPolicy;
},
checkCrossOriginOpenerPolicy: async () => {
if (typeof window === 'undefined') {
// Non-browser environment
crossOriginOpenerPolicy = 'non-browser-env';
return;
}

try {
const url = `${window.location.origin}${window.location.pathname}`;
const response = await fetch(url, {
method: 'HEAD',
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = response.headers.get('Cross-Origin-Opener-Policy');
crossOriginOpenerPolicy = result ?? 'null';

if (crossOriginOpenerPolicy === 'same-origin') {
console.error(COOP_ERROR_MESSAGE);
}
} catch (error) {
console.error('Error checking Cross-Origin-Opener-Policy:', (error as Error).message);
crossOriginOpenerPolicy = 'error';
}
},
};
};

export const { checkCrossOriginOpenerPolicy, getCrossOriginOpenerPolicy } = createCoopChecker();
15 changes: 0 additions & 15 deletions packages/wallet-sdk/src/util/crossOriginOpenerPolicy.ts

This file was deleted.

8 changes: 6 additions & 2 deletions packages/wallet-sdk/src/util/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LIB_VERSION } from '../version';
import { NAME, VERSION } from '../sdk-info';
import { standardErrors } from ':core/error';
import { ConstructorOptions, ProviderInterface, RequestArguments } from ':core/provider/interface';

Expand All @@ -12,7 +12,11 @@ export async function fetchRPCRequest(request: RequestArguments, rpcUrl: string)
method: 'POST',
body: JSON.stringify(requestBody),
mode: 'cors',
headers: { 'Content-Type': 'application/json', 'X-Cbw-Sdk-Version': LIB_VERSION },
headers: {
'Content-Type': 'application/json',
'X-Cbw-Sdk-Version': VERSION,
'X-Cbw-Sdk-Platform': NAME,
},
});
const { result, error } = await res.json();
if (error) throw error;
Expand Down
14 changes: 14 additions & 0 deletions packages/wallet-sdk/src/util/web.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { NAME, VERSION } from 'src/sdk-info';

import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy';
import { closePopup, openPopup } from './web';
import { standardErrors } from ':core/error';

jest.mock('./checkCrossOriginOpenerPolicy');
(getCrossOriginOpenerPolicy as jest.Mock).mockReturnValue('null');

const mockOrigin = 'http://localhost';

describe('PopupManager', () => {
beforeAll(() => {
global.window = Object.create(window);
Expand All @@ -11,6 +19,7 @@ describe('PopupManager', () => {
screenY: { value: 0 },
open: { value: jest.fn() },
close: { value: jest.fn() },
location: { value: { origin: mockOrigin } },
});
});

Expand All @@ -31,6 +40,11 @@ describe('PopupManager', () => {
'width=420, height=540, left=302, top=114'
);
expect(popup.focus).toHaveBeenCalledTimes(1);

expect(url.searchParams.get('sdkName')).toBe(NAME);
expect(url.searchParams.get('sdkVersion')).toBe(VERSION);
expect(url.searchParams.get('origin')).toBe(mockOrigin);
expect(url.searchParams.get('coop')).toBe('null');
});

it('should throw an error if popup fails to open', () => {
Expand Down
Loading

0 comments on commit 8386209

Please sign in to comment.