Skip to content

Commit 8f193ff

Browse files
authored
Add WooPay Express Checkout functionality to shortcode and cart (#5164)
1 parent e4e9f3c commit 8f193ff

10 files changed

+312
-10
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: add
3+
4+
Add WooPay Express Checkout button iframe functionality.

client/checkout/api/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getPaymentRequestAjaxURL,
1010
buildAjaxURL,
1111
} from '../../payment-request/utils';
12+
import { getWooPayExpressData } from '../platform-checkout/express-button/utils';
1213
import { decryptClientSecret } from '../utils/encryption';
1314

1415
/**
@@ -654,10 +655,17 @@ export default class WCPayAPI {
654655
}
655656

656657
initPlatformCheckout( userEmail, platformCheckoutUserSession ) {
658+
// In case this is being called via WooPay Express Checkout button from a product page,
659+
// the getConfig function won't work, so fallback to getWooPayExpressData.
660+
const wcAjaxUrl =
661+
getWooPayExpressData( 'wcAjaxUrl' ) ?? getConfig( 'wcAjaxUrl' );
662+
const nonce =
663+
getWooPayExpressData( 'initPlatformCheckoutNonce' ) ??
664+
getConfig( 'initPlatformCheckoutNonce' );
657665
return this.request(
658-
buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'init_platform_checkout' ),
666+
buildAjaxURL( wcAjaxUrl, 'init_platform_checkout' ),
659667
{
660-
_wpnonce: getConfig( 'initPlatformCheckoutNonce' ),
668+
_wpnonce: nonce,
661669
email: userEmail,
662670
user_session: platformCheckoutUserSession,
663671
}

client/checkout/api/test/index.test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import WCPayAPI from '..';
55
import request from 'wcpay/checkout/utils/request';
66
import { buildAjaxURL } from 'wcpay/payment-request/utils';
77
import { getConfig } from 'wcpay/utils/checkout';
8+
import { getWooPayExpressData } from 'wcpay/checkout/platform-checkout/express-button/utils';
89

910
jest.mock( 'wcpay/checkout/utils/request', () => jest.fn() );
1011
jest.mock( 'wcpay/payment-request/utils', () => ( {
@@ -13,9 +14,12 @@ jest.mock( 'wcpay/payment-request/utils', () => ( {
1314
jest.mock( 'wcpay/utils/checkout', () => ( {
1415
getConfig: jest.fn(),
1516
} ) );
17+
jest.mock( 'wcpay/checkout/platform-checkout/express-button/utils', () => ( {
18+
getWooPayExpressData: jest.fn(),
19+
} ) );
1620

1721
describe( 'WCPayAPI', () => {
18-
test( 'initializes platform checkout using expected params', () => {
22+
test( 'initializes platform checkout using config params', () => {
1923
buildAjaxURL.mockReturnValue( 'https://example.org/' );
2024
getConfig.mockReturnValue( 'foo' );
2125

@@ -28,4 +32,18 @@ describe( 'WCPayAPI', () => {
2832
user_session: 'qwerty123',
2933
} );
3034
} );
35+
36+
test( 'initializes platform checkout using express checkout params', () => {
37+
buildAjaxURL.mockReturnValue( 'https://example.org/' );
38+
getWooPayExpressData.mockReturnValue( 'bar' );
39+
40+
const api = new WCPayAPI( {}, request );
41+
api.initPlatformCheckout( '[email protected]', 'qwerty123' );
42+
43+
expect( request ).toHaveBeenLastCalledWith( 'https://example.org/', {
44+
_wpnonce: 'bar',
45+
46+
user_session: 'qwerty123',
47+
} );
48+
} );
3149
} );
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { __ } from '@wordpress/i18n';
5+
import { getWooPayExpressData } from './utils';
6+
import wcpayTracks from 'tracks';
7+
8+
export const expressCheckoutIframe = async ( api ) => {
9+
let userEmail = '';
10+
11+
const spinner = document.createElement( 'div' );
12+
const parentDiv = document.body;
13+
spinner.classList.add( 'wc-block-components-spinner' );
14+
15+
// Make the otp iframe wrapper.
16+
const iframeWrapper = document.createElement( 'div' );
17+
iframeWrapper.setAttribute( 'role', 'dialog' );
18+
iframeWrapper.setAttribute( 'aria-modal', 'true' );
19+
iframeWrapper.classList.add( 'platform-checkout-otp-iframe-wrapper' );
20+
21+
// Make the otp iframe.
22+
const iframe = document.createElement( 'iframe' );
23+
iframe.title = __( 'WooPay SMS code verification', 'woocommerce-payments' );
24+
iframe.classList.add( 'platform-checkout-otp-iframe' );
25+
26+
// To prevent twentytwenty.intrinsicRatioVideos from trying to resize the iframe.
27+
iframe.classList.add( 'intrinsic-ignore' );
28+
29+
// Maybe we could make this a configurable option defined in PHP so it could be filtered by merchants.
30+
const fullScreenModalBreakpoint = 768;
31+
32+
// Track the current state of the header. This default
33+
// value should match the default state on the platform.
34+
let iframeHeaderValue = true;
35+
const getWindowSize = () => {
36+
if (
37+
( fullScreenModalBreakpoint <= window.innerWidth &&
38+
iframeHeaderValue ) ||
39+
( fullScreenModalBreakpoint > window.innerWidth &&
40+
! iframeHeaderValue )
41+
) {
42+
iframeHeaderValue = ! iframeHeaderValue;
43+
iframe.contentWindow.postMessage(
44+
{
45+
action: 'setHeader',
46+
value: iframeHeaderValue,
47+
},
48+
getWooPayExpressData( 'platformCheckoutHost' )
49+
);
50+
}
51+
52+
// Prevent scrolling when the iframe is open.
53+
document.body.style.overflow = 'hidden';
54+
};
55+
56+
/**
57+
* Handles setting the iframe position based on the window size.
58+
* It tries to be positioned at the center of the screen unless
59+
* window is smaller than breakpoint which makes it full window size.
60+
*/
61+
const setPopoverPosition = () => {
62+
// If for some reason the iframe is not loaded, just return.
63+
if ( ! iframe ) {
64+
return;
65+
}
66+
67+
// If the window width is less than the breakpoint, set iframe to full window
68+
if ( fullScreenModalBreakpoint >= window.innerWidth ) {
69+
iframe.style.left = '0';
70+
iframe.style.right = '';
71+
iframe.style.top = '0';
72+
return;
73+
}
74+
75+
// Get references to the iframe bounding rects.
76+
const iframeRect = iframe.getBoundingClientRect();
77+
78+
// Set the iframe top and left to be centered.
79+
iframe.style.top =
80+
Math.floor( window.innerHeight / 2 - iframeRect.height / 2 ) + 'px';
81+
iframe.style.left =
82+
Math.floor( window.innerWidth / 2 - iframeRect.width / 2 ) + 'px';
83+
};
84+
85+
iframe.addEventListener( 'load', () => {
86+
// Set the initial value.
87+
iframeHeaderValue = true;
88+
89+
getWindowSize();
90+
window.addEventListener( 'resize', getWindowSize );
91+
92+
setPopoverPosition();
93+
window.addEventListener( 'resize', setPopoverPosition );
94+
95+
iframe.classList.add( 'open' );
96+
wcpayTracks.recordUserEvent(
97+
wcpayTracks.events.PLATFORM_CHECKOUT_OTP_START
98+
);
99+
} );
100+
101+
// Add the iframe to the wrapper.
102+
iframeWrapper.insertBefore( iframe, null );
103+
104+
// Error message to display when there's an error contacting WooPay.
105+
const errorMessage = document.createElement( 'div' );
106+
errorMessage.style[ 'white-space' ] = 'normal';
107+
errorMessage.textContent = __(
108+
'WooPay is unavailable at this time. Please complete your checkout below. Sorry for the inconvenience.',
109+
'woocommerce-payments'
110+
);
111+
112+
const closeIframe = () => {
113+
window.removeEventListener( 'resize', getWindowSize );
114+
window.removeEventListener( 'resize', setPopoverPosition );
115+
116+
iframeWrapper.remove();
117+
iframe.classList.remove( 'open' );
118+
119+
document.body.style.overflow = '';
120+
};
121+
122+
iframeWrapper.addEventListener( 'click', closeIframe );
123+
124+
const openIframe = ( email ) => {
125+
const urlParams = new URLSearchParams();
126+
urlParams.append( 'email', email );
127+
urlParams.append(
128+
'needsHeader',
129+
fullScreenModalBreakpoint > window.innerWidth
130+
);
131+
urlParams.append(
132+
'wcpayVersion',
133+
getWooPayExpressData( 'wcpayVersionNumber' )
134+
);
135+
136+
iframe.src = `${ getWooPayExpressData(
137+
'platformCheckoutHost'
138+
) }/otp/?${ urlParams.toString() }`;
139+
140+
// Insert the wrapper into the DOM.
141+
parentDiv.insertBefore( iframeWrapper, null );
142+
143+
setPopoverPosition();
144+
145+
// Focus the iframe.
146+
iframe.focus();
147+
};
148+
149+
const showErrorMessage = () => {
150+
parentDiv.insertBefore( errorMessage );
151+
};
152+
153+
document.addEventListener( 'keyup', ( event ) => {
154+
if ( 'Escape' === event.key && closeIframe() ) {
155+
event.stopPropagation();
156+
}
157+
} );
158+
159+
window.addEventListener( 'message', ( e ) => {
160+
if (
161+
! getWooPayExpressData( 'platformCheckoutHost' ).startsWith(
162+
e.origin
163+
)
164+
) {
165+
return;
166+
}
167+
168+
switch ( e.data.action ) {
169+
case 'otp_email_submitted':
170+
userEmail = e.data.userEmail;
171+
break;
172+
case 'redirect_to_platform_checkout':
173+
wcpayTracks.recordUserEvent(
174+
wcpayTracks.events.PLATFORM_CHECKOUT_OTP_COMPLETE
175+
);
176+
api.initPlatformCheckout(
177+
userEmail,
178+
e.data.platformCheckoutUserSession
179+
).then( ( response ) => {
180+
if ( 'success' === response.result ) {
181+
window.location = response.url;
182+
} else {
183+
showErrorMessage();
184+
closeIframe( false );
185+
}
186+
} );
187+
break;
188+
case 'otp_validation_failed':
189+
wcpayTracks.recordUserEvent(
190+
wcpayTracks.events.PLATFORM_CHECKOUT_OTP_FAILED
191+
);
192+
break;
193+
case 'close_modal':
194+
closeIframe();
195+
break;
196+
case 'iframe_height':
197+
if ( 300 < e.data.height ) {
198+
if ( fullScreenModalBreakpoint <= window.innerWidth ) {
199+
// set height to given value
200+
iframe.style.height = e.data.height + 'px';
201+
202+
// center top in window
203+
iframe.style.top =
204+
Math.floor(
205+
window.innerHeight / 2 - e.data.height / 2
206+
) + 'px';
207+
} else {
208+
iframe.style.height = '';
209+
iframe.style.top = '';
210+
}
211+
}
212+
break;
213+
default:
214+
// do nothing, only respond to expected actions.
215+
}
216+
} );
217+
218+
window.addEventListener( 'pageshow', function ( event ) {
219+
if ( event.persisted ) {
220+
// Safari needs to close iframe with this.
221+
closeIframe( false );
222+
}
223+
} );
224+
225+
openIframe();
226+
};

client/checkout/platform-checkout/express-button/index.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,34 @@ import ReactDOM from 'react-dom';
66
/**
77
* Internal dependencies
88
*/
9+
import { getWooPayExpressData } from './utils';
910
import { WoopayExpressCheckoutButton } from './woopay-express-checkout-button';
11+
import WCPayAPI from '../../api';
12+
import request from '../../utils/request';
1013

1114
const renderPlatformCheckoutExpressButton = () => {
15+
// Create an API object, which will be used throughout the checkout.
16+
const api = new WCPayAPI(
17+
{
18+
publishableKey: getWooPayExpressData( 'publishableKey' ),
19+
accountId: getWooPayExpressData( 'accountId' ),
20+
forceNetworkSavedCards: getWooPayExpressData(
21+
'forceNetworkSavedCards'
22+
),
23+
locale: getWooPayExpressData( 'locale' ),
24+
},
25+
request
26+
);
27+
1228
const platformCheckoutContainer = document.getElementById(
1329
'wcpay-platform-checkout-button'
1430
);
1531

1632
if ( platformCheckoutContainer ) {
1733
ReactDOM.render(
1834
<WoopayExpressCheckoutButton
19-
buttonSettings={ global.wcpayWooPayExpressParams.button }
35+
buttonSettings={ getWooPayExpressData( 'button' ) }
36+
api={ api }
2037
/>,
2138
platformCheckoutContainer
2239
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* global wcpayWooPayExpressParams */
2+
3+
/**
4+
* Retrieves a configuration value.
5+
*
6+
* @param {string} name The name of the config parameter.
7+
* @return {*} The value of the parameter of null.
8+
*/
9+
export const getWooPayExpressData = ( name ) => {
10+
return wcpayWooPayExpressParams?.[ name ];
11+
};

client/checkout/platform-checkout/express-button/woopay-express-checkout-button.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { sprintf, __ } from '@wordpress/i18n';
77
* Internal dependencies
88
*/
99
import WoopayIcon from './woopay-icon';
10+
import { expressCheckoutIframe } from './express-checkout-iframe';
1011

1112
export const WoopayExpressCheckoutButton = ( {
1213
isPreview = false,
1314
buttonSettings,
15+
api,
1416
} ) => {
1517
const { type: buttonType, text, height, size, theme } = buttonSettings;
1618

@@ -21,7 +23,7 @@ export const WoopayExpressCheckoutButton = ( {
2123
return; // eslint-disable-line no-useless-return
2224
}
2325

24-
// Add buton functionality.
26+
expressCheckoutIframe( api );
2527
};
2628

2729
return (

0 commit comments

Comments
 (0)