diff --git a/README.md b/README.md index aee298d..a3e4b8b 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,6 @@ const options = { }, closeOnExit: true, closeOnSuccess: true, - embeddedContentStyles: { - target: '#target-area', - }, onExit: () => { alert('On Exit'); }, diff --git a/src/onramp/initOnRamp.ts b/src/onramp/initOnRamp.ts index a05b2af..c425f18 100644 --- a/src/onramp/initOnRamp.ts +++ b/src/onramp/initOnRamp.ts @@ -12,7 +12,7 @@ export type InitOnRampCallback = { export const initOnRamp = ( { - experienceLoggedIn = 'embedded', // default experience type + experienceLoggedIn = 'popup', // default experience type widgetParameters, ...options }: InitOnRampParams, diff --git a/src/types/widget.ts b/src/types/widget.ts index 9850deb..7f934b8 100644 --- a/src/types/widget.ts +++ b/src/types/widget.ts @@ -4,18 +4,10 @@ export type WidgetType = 'buy' | 'checkout'; export type IntegrationType = 'direct' | 'secure_standalone'; -export type Experience = 'embedded' | 'popup' | 'new_tab'; +export type Experience = 'popup' | 'new_tab'; export type Theme = 'light' | 'dark'; -export type EmbeddedContentStyles = { - target?: string; - width?: string; - height?: string; - position?: string; - top?: string; -}; - export type CBPayExperienceOptions = { widgetParameters: T; target?: string; @@ -29,7 +21,6 @@ export type CBPayExperienceOptions = { onRequestedUrl?: (url: string) => void; closeOnExit?: boolean; closeOnSuccess?: boolean; - embeddedContentStyles?: EmbeddedContentStyles; experienceLoggedIn?: Experience; experienceLoggedOut?: Experience; }; diff --git a/src/utils/CBPayInstance.ts b/src/utils/CBPayInstance.ts index 15ec3b5..4c4afa1 100644 --- a/src/utils/CBPayInstance.ts +++ b/src/utils/CBPayInstance.ts @@ -52,7 +52,6 @@ export class CBPayInstance implements CBPayInstanceType { widget, experienceLoggedIn, experienceLoggedOut, - embeddedContentStyles, onExit, onSuccess, onEvent, @@ -65,7 +64,6 @@ export class CBPayInstance implements CBPayInstanceType { path: widgetRoutes[widget], experienceLoggedIn, experienceLoggedOut, - embeddedContentStyles, onExit: () => { onExit?.(); if (closeOnExit) { diff --git a/src/utils/CoinbasePixel.test.ts b/src/utils/CoinbasePixel.test.ts index ca3d00a..8ca6aac 100644 --- a/src/utils/CoinbasePixel.test.ts +++ b/src/utils/CoinbasePixel.test.ts @@ -3,8 +3,8 @@ import { PIXEL_ID, CoinbasePixelConstructorParams, OpenExperienceOptions, + popupWindowFeatures, } from './CoinbasePixel'; -import { EMBEDDED_IFRAME_ID } from './createEmbeddedContent'; import { broadcastPostMessage, onBroadcastedPostMessage } from './postMessage'; @@ -25,13 +25,14 @@ describe('CoinbasePixel', () => { }; const defaultOpenOptions: OpenExperienceOptions = { path: '/buy', - experienceLoggedIn: 'embedded', + experienceLoggedIn: 'popup', }; beforeEach(() => { mockOnReady = jest.fn(); mockOnFallbackOpen = jest.fn(); mockUnsubCallback = jest.fn(); + (window.open as jest.Mock).mockReset(); (onBroadcastedPostMessage as jest.Mock).mockReturnValue(mockUnsubCallback); defaultArgs = { appId: 'test', @@ -43,7 +44,6 @@ describe('CoinbasePixel', () => { afterEach(() => { document.getElementById(PIXEL_ID)?.remove(); - document.getElementById(EMBEDDED_IFRAME_ID)?.remove(); // @ts-expect-error - test window.chrome = undefined; jest.resetAllMocks(); @@ -154,7 +154,11 @@ describe('CoinbasePixel', () => { pixel.openExperience(defaultOpenOptions); - expect(document.querySelector(`iframe#${EMBEDDED_IFRAME_ID}`)).toBeTruthy(); + expect(window.open).toHaveBeenCalledWith( + 'https://pay.coinbase.com/buy?appId=test&type=secure_standalone&nonce=mock-nonce', + 'Coinbase', + popupWindowFeatures, + ); }); it('should handle openExperience when pixel has status "loading"', () => { @@ -163,7 +167,7 @@ describe('CoinbasePixel', () => { expect(instance.state).toEqual('loading'); instance.openExperience(defaultOpenOptions); - expect(document.querySelector(`iframe#${EMBEDDED_IFRAME_ID}`)).toBeFalsy(); + expect(window.open).not.toHaveBeenCalled(); }); it('should handle openExperience when pixel has status "waiting_for_response"', () => { @@ -173,7 +177,7 @@ describe('CoinbasePixel', () => { instance.openExperience(defaultOpenOptions); expect(instance.queuedOpenOptions).toBeFalsy(); - expect(document.querySelector(`iframe#${EMBEDDED_IFRAME_ID}`)).toBeFalsy(); + expect(window.open).not.toHaveBeenCalled(); }); it('should handle openExperience when pixel has status "failed"', () => { @@ -183,7 +187,7 @@ describe('CoinbasePixel', () => { instance.openExperience(defaultOpenOptions); expect(instance.queuedOpenOptions).toBeFalsy(); - expect(document.querySelector(`iframe#${EMBEDDED_IFRAME_ID}`)).toBeFalsy(); + expect(window.open).not.toHaveBeenCalled(); }); it('should handle openExperience with no preloaded nonce', () => { @@ -195,22 +199,7 @@ describe('CoinbasePixel', () => { 'Attempted to open CB Pay experience without nonce', ); - expect(document.querySelector(`iframe#${EMBEDDED_IFRAME_ID}`)).toBeFalsy(); - }); - - it('should handle opening the embedded experience when logged out', () => { - const instance = createUntypedPixel(defaultArgs); - - mockPixelReady(false); - mockOnAppParamsNonce('mock-nonce'); - instance.openExperience(defaultOpenOptions); - - expect(window.open).toHaveBeenCalledWith( - 'https://pay.coinbase.com/signin?appId=test&type=direct', - 'Coinbase', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, height=730,width=460', - ); - expect(findMockedListeners('signin_success')).toHaveLength(1); + expect(window.open).not.toHaveBeenCalled(); }); it('should handle opening the popup experience in chrome extensions', () => { @@ -271,7 +260,7 @@ describe('CoinbasePixel', () => { expect(window.open).toHaveBeenCalledWith( 'https://pay.coinbase.com/buy?appId=test&type=secure_standalone&nonce=mock-nonce', 'Coinbase', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, height=730,width=460', + popupWindowFeatures, ); }); @@ -358,7 +347,3 @@ function mockOnAppParamsNonce(nonce: string) { call[1].onMessage({ nonce }); }); } - -function findMockedListeners(message: string) { - return (onBroadcastedPostMessage as jest.Mock).mock.calls.filter(([m]) => m === message); -} diff --git a/src/utils/CoinbasePixel.ts b/src/utils/CoinbasePixel.ts index e0199df..22f6ab7 100644 --- a/src/utils/CoinbasePixel.ts +++ b/src/utils/CoinbasePixel.ts @@ -1,6 +1,5 @@ import { DEFAULT_HOST } from '../config'; -import { EmbeddedContentStyles, Experience, Theme } from 'types/widget'; -import { createEmbeddedContent, EMBEDDED_IFRAME_ID } from './createEmbeddedContent'; +import { Experience, Theme } from 'types/widget'; import { JsonObject } from 'types/JsonTypes'; import { broadcastPostMessage, onBroadcastedPostMessage } from './postMessage'; import { EventMetadata } from 'types/events'; @@ -44,9 +43,21 @@ export type OpenExperienceOptions = { path: string; experienceLoggedIn: Experience; experienceLoggedOut?: Experience; - embeddedContentStyles?: EmbeddedContentStyles; } & ExperienceListeners; +export const popupWindowFeatures = [ + 'copyhistory=no', + 'directories=no', + 'location=no', + 'menubar=no', + 'resizable=no', + 'scrollbars=no', + 'status=no', + 'toolbar=no', + `height=${PopupSizes.signin.height}`, + `width=${PopupSizes.signin.width}`, +].join(', '); + export class CoinbasePixel { /** * Tracks the loading state of the embedded pixel @@ -71,6 +82,7 @@ export class CoinbasePixel { private onReadyCallback: CoinbasePixelConstructorParams['onReady']; private onFallbackOpen: CoinbasePixelConstructorParams['onFallbackOpen']; private theme: Theme | null | undefined; + private widgetWindow: Window | null; public isLoggedIn = false; @@ -90,6 +102,7 @@ export class CoinbasePixel { this.onFallbackOpen = onFallbackOpen; this.debug = debug || false; this.theme = theme; + this.widgetWindow = null; this.addPixelReadyListener(); this.addErrorListener(); @@ -132,7 +145,7 @@ export class CoinbasePixel { this.setupExperienceListeners(options); - const { path, experienceLoggedIn, experienceLoggedOut, embeddedContentStyles } = options; + const { path, experienceLoggedIn, experienceLoggedOut } = options; const widgetUrl = new URL(`${this.host}${path}`); widgetUrl.searchParams.append('appId', this.appId); @@ -151,23 +164,7 @@ export class CoinbasePixel { this.log('Opening experience', { experience, isLoggedIn: this.isLoggedIn }); - if (experience === 'embedded') { - const openEmbeddedExperience = () => { - const embedded = createEmbeddedContent({ url, ...embeddedContentStyles }); - if (embeddedContentStyles?.target) { - document.querySelector(embeddedContentStyles?.target)?.replaceChildren(embedded); - } else { - document.body.appendChild(embedded); - } - }; - - if (!this.isLoggedIn) { - // Embedded experience opens popup for signin - this.startDirectSignin(openEmbeddedExperience); - } else { - openEmbeddedExperience(); - } - } else if (experience === 'popup' && window.chrome?.windows?.create) { + if (experience === 'popup' && window.chrome?.windows?.create) { void window.chrome.windows.create( { url, @@ -195,7 +192,7 @@ export class CoinbasePixel { } else if (experience === 'new_tab' && window.chrome?.tabs?.create) { void window.chrome.tabs.create({ url }); } else { - openWindow(url, experience); + this.widgetWindow = openWindow(url, experience); } // For users who exit the experience and want to re-enter, we need a fresh nonce to use. @@ -209,7 +206,8 @@ export class CoinbasePixel { }; public endExperience = (): void => { - document.getElementById(EMBEDDED_IFRAME_ID)?.remove(); + this.widgetWindow?.close(); + this.widgetWindow = null; }; public destroy = (): void => { @@ -394,11 +392,5 @@ function createPixel({ host, appId }: { host: string; appId: string }) { } function openWindow(url: string, experience: Experience) { - return window.open( - url, - 'Coinbase', - experience === 'popup' - ? `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, height=${PopupSizes.signin.height},width=${PopupSizes.signin.width}` - : undefined, - ); + return window.open(url, 'Coinbase', experience === 'popup' ? popupWindowFeatures : undefined); } diff --git a/src/utils/createEmbeddedContent.test.ts b/src/utils/createEmbeddedContent.test.ts deleted file mode 100644 index 077c099..0000000 --- a/src/utils/createEmbeddedContent.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createEmbeddedContent, EMBEDDED_IFRAME_ID } from './createEmbeddedContent'; - -describe('createEmbeddedContent', () => { - it('creates iframe with given URL', () => { - const iframe = createEmbeddedContent({ url: TEST_URL }); - expect(iframe.src).toMatch(TEST_URL); - }); - - it('gives the iframe a fixed ID', () => { - const iframe = createEmbeddedContent({ url: TEST_URL }); - expect(iframe.id).toBe(EMBEDDED_IFRAME_ID); - }); - - it('should have default styles (full screen)', () => { - const iframe = createEmbeddedContent({ url: TEST_URL }); - - const { width, height, position, top, border, borderWidth } = iframe.style; - expect(width).toBe('100%'); - expect(height).toBe('100%'); - expect(position).toBe('fixed'); - expect(top).toBe('0px'); - expect(border).toBe('0px'); - expect(borderWidth).toBe('0px'); - }); - - it('allows overriding of some key style properties', () => { - const iframe = createEmbeddedContent({ - url: TEST_URL, - width: '50px', - height: '30vh', - position: 'absolute', - top: '18px', - }); - - const { width, height, position, top, border, borderWidth } = iframe.style; - expect(width).toBe('50px'); - expect(height).toBe('30vh'); - expect(position).toBe('absolute'); - expect(top).toBe('18px'); - // Border should not be changed - expect(border).toBe('0px'); - expect(borderWidth).toBe('0px'); - }); -}); - -const TEST_URL = 'https://foo.bar'; diff --git a/src/utils/createEmbeddedContent.ts b/src/utils/createEmbeddedContent.ts deleted file mode 100644 index ecd6e61..0000000 --- a/src/utils/createEmbeddedContent.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EmbeddedContentStyles } from 'types/widget'; - -export const EMBEDDED_IFRAME_ID = 'cbpay-embedded-onramp'; - -export const createEmbeddedContent = ({ - url, - width = '100%', - height = '100%', - position = 'fixed', - top = '0px', -}: { - url: string; -} & EmbeddedContentStyles): HTMLIFrameElement => { - const iframe = document.createElement('iframe'); - - // Styles - iframe.style.border = 'unset'; - iframe.style.borderWidth = '0'; - iframe.style.width = width.toString(); - iframe.style.height = height.toString(); - iframe.style.position = position; - iframe.style.top = top; - iframe.id = EMBEDDED_IFRAME_ID; - iframe.src = url; - - return iframe; -};