@@ -6,8 +6,21 @@ import { CommonHooks, OnboardingHooks } from '@ppcp-settings/data';
66import { useStoreManager } from './useStoreManager' ;
77import useNotices from './useNotices' ;
88
9- const PAYPAL_PARTNER_SDK_URL =
10- 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js' ;
9+ /**
10+ * PayPal's onboarding SDK (partner.js) is environment-specific and offers no
11+ * namespace support: it always uses the single `window.PAYPAL` global, derives
12+ * the environment from its own script domain, and only wires up the *first*
13+ * `[data-paypal-button]` element it finds. We therefore load the SDK for the
14+ * active environment and make sure only that environment's button is a real
15+ * `[data-paypal-button]`, so partner.js binds the correct button to the
16+ * minibrowser (see `useHandleOnboardingButton`).
17+ */
18+ const PAYPAL_PARTNER_SDK_URL = {
19+ production :
20+ 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js' ,
21+ sandbox :
22+ 'https://www.sandbox.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js' ,
23+ } ;
1124
1225const MESSAGES = {
1326 CONNECTED : __ ( 'Connected to PayPal' , 'woocommerce-paypal-payments' ) ,
@@ -31,10 +44,40 @@ const ACTIVITIES = {
3144 API_VERIFY : 'auth/verify-login' ,
3245} ;
3346
47+ /**
48+ * Environment ('sandbox' | 'production') of the connection button the merchant
49+ * actually clicked.
50+ *
51+ * PayPal's partner.js exposes a single global onOnboardComplete callback, but the
52+ * onboarding step mounts a live and a sandbox button at the same time. This
53+ * module-level value lets the shared completion handler authenticate against the
54+ * environment the merchant chose, rather than whichever button registered its
55+ * handler first.
56+ *
57+ * @type {?string }
58+ */
59+ let clickedEnvironment = null ;
60+
61+ export const setClickedEnvironment = ( environment ) => {
62+ clickedEnvironment = environment ;
63+ } ;
64+
3465export const useHandleOnboardingButton = ( isSandbox ) => {
3566 const { onboardingUrl } = isSandbox
3667 ? CommonHooks . useSandbox ( )
3768 : CommonHooks . useProduction ( ) ;
69+ const { isSandboxMode } = CommonHooks . useSandbox ( ) ;
70+
71+ /**
72+ * partner.js only wires up a single button and derives its environment from
73+ * its own script domain, so the live and sandbox SDKs cannot coexist. Only
74+ * the button whose environment matches the "Enable Sandbox Mode" toggle is
75+ * the active one: it loads the matching SDK and is rendered as a real
76+ * `[data-paypal-button]`. The other button is rendered inert (no href /
77+ * data-attributes) so partner.js ignores it.
78+ */
79+ const isActiveEnvironment = isSandbox === ! ! isSandboxMode ;
80+
3881 const { ownBrandOnly, storeCountry } = CommonHooks . useWooSettings ( ) ;
3982 const { products, options } = OnboardingHooks . useDetermineProducts (
4083 ownBrandOnly ,
@@ -69,85 +112,92 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
69112
70113 useEffect ( ( ) => {
71114 /**
72- * The partner.js script initializes all onboarding buttons in the onload event.
73- * When no buttons are present, a JS error is displayed; i.e. we should load this script
115+ * The partner.js script initializes the onboarding button in its onload event.
116+ * When no button is present, a JS error is displayed; i.e. we should load this script
74117 * only when the button is ready (with a valid href and data-attributes).
118+ *
119+ * We load the SDK only for the button matching the active environment. Because
120+ * partner.js keeps its state on the single `window.PAYPAL` global (and derives
121+ * the environment from its own script domain), we reset that global and remove
122+ * the previously injected scripts before loading, so switching environments
123+ * re-initializes cleanly against the correct domain.
75124 */
76- if ( ! onboardingUrlState ) {
125+ if ( ! onboardingUrlState || ! isActiveEnvironment ) {
77126 return ;
78127 }
79128
129+ // partner.js injects signup-js and rampConfig-js itself; remove all three
130+ // so switching between environments starts from a clean slate.
131+ const removeOnboardingScripts = ( ) => {
132+ [
133+ 'partner-js' ,
134+ 'signup-js' ,
135+ 'rampConfig-js' ,
136+ 'zoidMiniBrowser-js' ,
137+ ] . forEach ( ( id ) => {
138+ const el = document . querySelector ( `script[id="${ id } "]` ) ;
139+
140+ if ( el ?. parentNode ) {
141+ el . parentNode . removeChild ( el ) ;
142+ }
143+ } ) ;
144+ } ;
145+
146+ removeOnboardingScripts ( ) ;
147+ delete window . PAYPAL ;
148+
80149 const script = document . createElement ( 'script' ) ;
81150 script . id = 'partner-js' ;
82- script . src = PAYPAL_PARTNER_SDK_URL ;
151+ script . src = isSandbox
152+ ? PAYPAL_PARTNER_SDK_URL . sandbox
153+ : PAYPAL_PARTNER_SDK_URL . production ;
83154 script . onload = ( ) => {
84155 setScriptLoaded ( true ) ;
85156 } ;
86157 document . body . appendChild ( script ) ;
87158
88159 return ( ) => {
160+ removeOnboardingScripts ( ) ;
161+ delete window . PAYPAL ;
162+ setScriptLoaded ( false ) ;
163+ } ;
164+ } , [ onboardingUrlState , isActiveEnvironment , isSandbox ] ) ;
165+
166+ const setCompleteHandler = useCallback ( ( ) => {
167+ const onComplete = async ( authCode , sharedId ) => {
89168 /**
90- * When the component is unmounted, remove the partner.js script, as well as the
91- * dynamic scripts it loaded (signup-js and rampConfig-js)
169+ * Until now, the full page is blocked by PayPal's semi-transparent, black overlay.
170+ * But at this point, the overlay is removed, while we process the sharedId and
171+ * authCode via a REST call.
92172 *
93- * This is important, as the onboarding button is only initialized during the onload
94- * event of those scripts; i.e. we need to load the scripts again, when the button is
95- * rendered again .
173+ * Note: The REST response is irrelevant, since PayPal will most likely refresh this
174+ * frame before the REST endpoint returns a value. Using "withActivity" is more of a
175+ * visual cue to the user that something is still processing in the background .
96176 */
97- const onboardingScripts = [
98- 'partner-js' ,
99- 'signup-js' ,
100- 'rampConfig-js' ,
101- ] ;
102-
103- onboardingScripts . forEach ( ( id ) => {
104- const el = document . querySelector ( `script[id="${ id } "]` ) ;
177+ startActivity (
178+ ACTIVITIES . OAUTH_VERIFY ,
179+ 'Validating the connection details'
180+ ) ;
105181
106- if ( el ?. parentNode ) {
107- el . parentNode . removeChild ( el ) ;
108- }
109- } ) ;
182+ await authenticateWithOAuth (
183+ sharedId ,
184+ authCode ,
185+ clickedEnvironment === 'sandbox'
186+ ) ;
110187 } ;
111- } , [ onboardingUrlState ] ) ;
112-
113- const setCompleteHandler = useCallback (
114- ( environment ) => {
115- const onComplete = async ( authCode , sharedId ) => {
116- /**
117- * Until now, the full page is blocked by PayPal's semi-transparent, black overlay.
118- * But at this point, the overlay is removed, while we process the sharedId and
119- * authCode via a REST call.
120- *
121- * Note: The REST response is irrelevant, since PayPal will most likely refresh this
122- * frame before the REST endpoint returns a value. Using "withActivity" is more of a
123- * visual cue to the user that something is still processing in the background.
124- */
125- startActivity (
126- ACTIVITIES . OAUTH_VERIFY ,
127- 'Validating the connection details'
128- ) ;
129-
130- await authenticateWithOAuth (
131- sharedId ,
132- authCode ,
133- environment === 'sandbox'
134- ) ;
135- } ;
136188
137- const addHandler = ( ) => {
138- const MiniBrowser = window . PAYPAL ?. apps ?. Signup ?. MiniBrowser ;
139- if ( ! MiniBrowser || MiniBrowser . onOnboardComplete ) {
140- return ;
141- }
189+ const addHandler = ( ) => {
190+ const MiniBrowser = window . PAYPAL ?. apps ?. Signup ?. MiniBrowser ;
191+ if ( ! MiniBrowser || MiniBrowser . onOnboardComplete ) {
192+ return ;
193+ }
142194
143- MiniBrowser . onOnboardComplete = onComplete ;
144- } ;
195+ MiniBrowser . onOnboardComplete = onComplete ;
196+ } ;
145197
146- // Ensure the onComplete handler is not removed by a PayPal init script.
147- timerRef . current = setInterval ( addHandler , 250 ) ;
148- } ,
149- [ authenticateWithOAuth , startActivity ]
150- ) ;
198+ // Ensure the onComplete handler is not removed by a PayPal init script.
199+ timerRef . current = setInterval ( addHandler , 250 ) ;
200+ } , [ authenticateWithOAuth , startActivity ] ) ;
151201
152202 const removeCompleteHandler = useCallback ( ( ) => {
153203 if ( timerRef . current ) {
@@ -161,6 +211,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
161211 return {
162212 onboardingUrl : onboardingUrlState ,
163213 scriptLoaded,
214+ isActiveEnvironment,
164215 setCompleteHandler,
165216 removeCompleteHandler,
166217 } ;
0 commit comments