# PayPal NextGen Integration Pattern Ideation ## Philosophy 1. Don't optimize for the least amount of code, optimize for clarity of intent 2. Make the impossible impossible, ideally caught before runtime (fail fast if at runtime) 3. Empower merchant developers to handle errors at every step 4. Discrete flows are ideal versus giant config interfaces ## Thoughts - Document why client integration is safe for BT integration (payments/capture happen with total price) - Document: Redirect vs In-Context - If we do redirect, we cannot support shipping callback in browser - If we do in-context, we have issues with Webviews - esp because we cannot have cookies or identification of buyer (no LLS) - Can we support some sort of redirect when necessary? - Question: Google Pay allows redirect for webviews- how do they do shipping callback in that scenario? - Stripe always allows a webhook for shipping changes - Document around Native vs Web restrictions - There are situations on native/mobile web boundary where payment handler won't work ## Considerations - This guide focuses more heavily on standalone button use cases that enable BT merchants to use Next Gen SDK for PayPal button - Buttons are designed as presentation and payment flows are attached to presentational buttons ## Required Work to make this happen 1. Identity to provide an endpoint and infrastructre to create the `sdkToken` we need for initialization 1. Orders API to provide capture validation for Orders created client-side via SDK helpers 1. Find out what we need to enable order creation earlier in the flow. Ex: on checkout page load ## Instrumentation We are providing a lot of merchant flexibility so we have some less control. We may not be able to instrument the same level of data as v5 today. TODO: go into depth on this topic! ## Integrations ### Initializing the SDK A secure server-side token must be created and passed to your front-end to initialize the SDK. For performance we reccomend that you pass it from your backend to the client in the html template you send though you can use a `window.fetch` request to dynamically retrieve the token from your backend. Using `window.fetch` will add some latency however. #### Generate your SDK Token SDK Token is a publically available universal access token (UAT) to use for the buyer's sdk session. This is web safe and does have a TTL of 15 minutes (to be defined). There is no refresh token, after TTL you must make a new token. #### Security Model - This token is bound to your domain - The payload is cryptographically secured when signed - Merchant provided URLs allow us to enable strict CORs rules to prevent impersonation ##### Code Samples *Discussion Notes:* - TODO: get type of token into this call so we can clarify - NOTE: Merchant needs to get an SDK token as well as a server-side access token ```shell curl -v -X POST https://paypal.com/v1/oauth2/token \ -H 'Content-Type: application/json' \ -H 'Authorization: Basic <CLIENT_ID>:<CLIENT_SECRET>' \ -d '{ "customerId": "<CUSTOMER_ID>", "merchantUrl": "<URL_FOR_THE_SESSION>" }' ``` Discussion Notes: 1. TODO: add the script tag to showcase the script getting to the page 2. Add notes about dynamic fetching ability of getting additional sdk features 3. AVOID: to many combinations. merchant goes to cdn and if we dont have it go to data center etc ```js // this is an example where the token is injected at buildtime, but there are many mechanisms a merchant dev can use const sdkToken = '<%= sdkToken %>'; try { const sdkInstance = await window.paypal.create({ sdkToken, }); } catch(e) { // {code: 'IN01', message: 'Your token is invalid', name: 'PAYPALINITERROR'} } // paypalInstance now contains our integration methods and utilities ``` ### Creating Server-Side Orders Same as you always have before just ensure you pass `orderId` to the front-end. ```js app.get('/create-order', function(req, res) { const url = `${base}/v2/checkout/orders`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, body: JSON.stringify({ // ... }), }, }); return handleResponse(response); }); ``` ### Creating Client-Side Orders After initializing the SDK you have access to the `sdkInstance.order.*` helper utilities which can be used to create and update orders. These helpers are limited when compared to server-side APIs but will provide a convient and secure way to interact with orders from the client-side. In general we still reccomend creating your order server-side when possible. #### Create Order ```js const orderId = sdkInstance.order.create({ currency: 'USD', amount: 5.00, vault: { storeInVault: 'on_success', description: 'Text to describe the saved payment method' } }); ``` #### Patch Order **Discussion Notes:** Merchant gets zipcode, calls ups to get shipping data, then patches order. this typically happens all server-side ```js const orderId = sdkInstance.order.patch(orderId, { currency: 'USD', amount: 5.00, vault: { storeInVault: 'on_success', description: 'Text to describe the saved payment method' } }); ``` #### Capture Order Capturing your order must be done server-side and amount must be passed in during capture for validation. ```js app.get('/capture', function(req, res) { const url = `${base}/v2/checkout/orders/${orderID}/capture`; const accessToken = base64(clientId + clientSecret); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, body: JSON.stringify({ purchase_units: [{ reference_id: "d9f80740-38f0-11e8-b467-0ed5f89f718b", amount: { currency_code: "USD", value: "100.00" } }] }), }, }); if (!response.ok) { // abbreviated error check // if the created amount does not match the captured amount we should expect an error throw new Error("Capture amount does not match."); } return handleResponse(response); }); ``` ### Creating Standalone Payment Components #### Rendering the Button Buttons are statically defined and able to be joined to payment flows with ease. Below we will demonstrate rendering a PayPal Button. **Discussion Notes:** 1. TODO: document the separation of UI and button logic 2. TODO: recap web-components render lifecycle ##### Code Sample ```html <!-- Loading the script tag --> <head> <script src="https://www.paypal.com/sdk/js/v6"></script> </head> <!-- Styling the button --> <style> -paypal-button-color: yellow; -paypal-button-radius: medium; </style> <!-- Rending the Web Component --> <body> <paypal-button id="paypal-button" type="payment" /> </body> ``` #### Client-Side Integration (minimal): ##### Open Questions 1. Can `oncomplete` replace both `onApprove` & `onCapture`? 1. When is the right time to create the order? Previously we've always required buyer intent, but should we maybe save a data structure in memory or allow up-front order creation? 1. Can we go in depth in internal doc about what we measure & track in our instrumentation? Can we ensure we are still going to get the analytics we need in the new integration patterns? 1. ##### Code Samples ```javascript // GOALS // 1. user can do an async function for the payment flow if they need to without impacting the transient activation // 2. Orders // - creating an order is async if done server-side onclick -> create order on page load // - creating an order client-side, before window launch, in the sdk can be sync (in-memory) or async (we call orders v2) // - creating an order client-side, after window launch, this removes our responsbility and we just hold the data // // - at popup launch we create the order from the data shape which opens the possibility of checkout doing it later. or edge // - // // 3. minimize the callback patterns - in the strictest sense of callback (a function attached from the merchant) // - so thinks that are bidi might be unavoidable like shipping change // - things like createOrder, onComplete, et all we might be able to make more procedural // 4. thinking about order being attached later // Should we have two different patterns: // 1. [New Model]: creating an order: client-side or server-side (on page load) // 2. [onClick]: server-side (current model more or less) // Below showcases [New Model] // https://developer.mozilla.org/en-US/docs/Web/API/PaymentRequest/show const paypalButton = document.getElementById('paypal-button'); // client-side helper const order = sdkInstance.order.create({ /*...*/ }); // this creates an in memory order so its sync. TODO: rename to reflect in memory object // fetch from merchant serverside const order = await fetch('my-server/get-order-id'); try { // this sets up the global sdk const sdkInstance = await window.paypal.create({ sdkToken, }); } catch(e) { // {code: 'IN01', message: 'Your token is invalid', name: 'PAYPALINITERROR'} } // Paypal Checkout Product Configuration try { const paypalCheckout = sdkInstance.createCheckout({ // string would be an order created server-side, PartialOrder would be the in-memory shape of an order we will create on your behalf // order: String | PartialOrder, order, ...rest }); } catch(e) { // Client-Side API Failures // Payment Flow Launching Errors // { code: PF02, message: "Launch Payment failed. no popup", name: "PayPalPaymentLaunchError"} } async function onShippingAddressChange(orderId, shippingData) { // merchant implementation typically uses orderId to patch order based on postalcode } async function onComplete(orderId) { // TODO: do they ever need more than orderId? await fetch('/merchant-server-complete-payment', { orderId: '<ORDER_ID>', }) }; paypalButton.addEventListener('click', paypalCheckout.start); // this is a new section // <button>click me</button> // 1. we have to spawn the popup on click event. so we have extra code in our button // 2. we paypalButton.addEventListener('click', async function() { const {orderId} = await paypalCheckout.start(); await fetch(...); }) function doPayPalFlow() { // merchant has to know nothing async can happen here or maybe we can call our internal click handler first then call theirs. that means ours can launch the popup while their async code runs // // Need to make it impossible for the merchant to get the popup blocked // // user clicks ---> paypal private on click opens popup ---> merchant click handler can be async now try { const order = sdkInstance.order.create({ /*...*/ }); // this creates an in memory order so its sync. TODO: rename to reflect in memory object /** * Launch the Payment Handler | Popup | Modal */ const session = sdkInstance.launchPayPal({ order: String | PartialOrder, // orderId: serverSideOrderId, // "123456" // order: sdkInstance.order, // { purahse_units: {}, ...} ...{ /* rest of your PayPalCheckout configs */ }, }); /** * Attach any relevant callbacks we need for the flow */ session.onShippingAddressChange = onShippingAddressChange; /** * Send orderId, setupToken, and customerId back to server to complete payment */ session.onComplete = onComplete; // Different layers of this // 1. create a paypal instance // 2. from here are we creating a session per payment type? is it all one session? // 3. } catch(e) { // Client-Side API Failures // { code: OF01, message: "Order creation failed", name: "PayPalOrderError"} // { code: BA01, message: "Failed to create Billing Agreement", name: "PayPalBillingAgreementError"} // // Payment Flow Launching Errors // { code: PF02, message: "Launch Payment failed", name: "PayPalPaymentLaunchError"} } } paypalButton.addEventListener('click', doPayPalFlow); ``` ### Creating Button Groups ```html <paypal-button-group> <paypal-button></paypal-button> <venmo-button></venmo-button> </paypal-button-group> ``` ```js // TODO: Button Groups! // Description: Multiple buttons are created and we provide a convenient way to manage those + the payment flows ``` #### Creating Card Fields ```js // TODO: Card Fields! ``` ### Shopper Insights We can likely match the current v5 spec for this as it fits nicely currently. ```js try { const { isInPayPalNetwork } = await sdkInstance.shopperInsights.getRecommendedPaymentMethods({ customer: { email: "customer12345@gmail.com", phone: { countryCode: "1", nationalNumber: "1234567890", }, }, }); }); } catch(e) { // { code SI01, message: "Merchant not onboarded", name: "ShopperInsightsError"} } ``` ### Session Events ```js session.oncomplete: (orderId) => void; session.onshippingaddresschange: (orderId, shippingInfo) => void; session.onshippingmethodchange: (orderId, shippingInfo) => void; session.oncancel: (orderId) => void; ``` ## Error Handling Because this follows a more procedural flow we can capture errors at each step ### Error Types #### Open Questions ```js class PayPalSDKError extends Error { /** * Publically documented error code that can be used to lookup additional information or troubleshooting guides. * Merchant can use these to generate their own helpful error message for buyers. * * These should NOT be displayed to the buyer. * */ code: string; /** * Category of the error for instance if its related to the payment after click it would be `PayPalPaymentError`. * * These should NOT be displayed to the buyer. */ name: string; /** * Message to assist merchant developers in their integrations. Ideally these should be actionable but at the very least * must be informative. * * These should NOT be displayed to the buyer. */ message: string; } ``` ### Error during Payment ```js try { const { orderId } = await paypal.launchPayment({ orderId, }); } catch (e) { console.log(e.code); // "PF01" console.log(e.name); // "PayPalPaymentFlowError" console.log(e.message); // A popup blocker may have blocked the request. Ensure onclick is synchrnous. } ``` ## Unlocks 1. Break everything out into discrete modules 2. Do not coordinate creation & approval inside the button, require order created in advance or client-side integration 3. Utilize a session for error handling 4. Break out buttons into web components and use native handling ## Type Definitions ```Typescript interface PayPalPaymentSession { status: 'not_started' | 'success' | 'canceled' | 'errored'; // can offer more orderId?: string; vaultSetupToken?: string; customerId?: string; } interface TokenizeSession { status: 'not_started' | 'success' | 'canceled' | 'errored'; vaultSetupToken: string; } ``` ## Naming conventions 1. When any variable uses an identifier such as `userId`, We always use an uppercase `I` and a lowercase `d`. 3. Url in a variable name is always spelled as `Url` [Inspiration](https://google.github.io/styleguide/jsguide.html#naming-camel-case-defined) ## Alternative Integrations ### Creating Standalone Payment Components ##### Code Sample ```html <head> <script src="paypal.com/sdk/<version>"></script> </head> ``` ```js try { const button = paypalInstance.Buttons.PayPal({ styles: { color: 'yellow', radius: 'medium' }, type: 'payment', // .. for Pay with PayPal, Book with Paypal etc }) button.onclick = function() { /*...*/ } await button.render('#paypal-button'); } catch (e) { // { code: BT01, message: "Button container not found", type: "PayPal Buttons could not find container to render"} } ``` ## Appendix Stuff we had that I moved and didn't want to lose for now.. ### TODOs 1. Can we show the web component rendering with styles as props and styles in JS. esentially the permutations of styling with web-component ### Attach Payment Flows to the Button ```javascript const paypalButton = document.getElementById('paypal-button'); paypalButton.addEventListener('click', async ()=> { const { setupToken } = await paypal.tokenize({ //configuration }); }) ``` ### Client-Side Integration With ShippingChange: ```javascript const paypalButton = document.getElementById('paypal-button'); paypalButton.addEventListener('click', async ()=> { const order = paypal.order({ // this is action currency: 'USD', amount: 10, vault: { storeInVault: 'on_success', description: 'Text to describe the saved payment method' } }); const session = await paypal.payment({ order, }); session.onShippingAddressChange = (order, previousAddress, newAddress) => { paypal.order.patch(order.id, {}); }; session.onShippingOptionChange = (order, previousAddress, newAddress) => { paypal.order.patch(order.id, {}) }; session.onComplete = () => { if (session.status !== 'success') { // Do error handling } // Send orderId, setupToken, and customerId back to server to complete payment // Requires merchant to capture with final total } }); ``` ## Analysis ### Current Integration Pattern ```javascript braintree.client.create({ authorization: 'authorization' }).then(function (clientInstance) { return braintree.paypalCheckout.create({ client: clientInstance }); }).then(function (paypalCheckoutInstance) { return paypal.Buttons({ createOrder: function () { return paypalCheckoutInstance.createPayment({ flow: 'checkout', currency: 'USD', amount: '10.00', intent: 'capture' // this value must either be `capture` or match the intent passed into the PayPal SDK intent query parameter // your other createPayment options here }); }, onApprove: function (data, actions) { // some logic here before tokenization happens below return paypalCheckoutInstance.tokenizePayment(data).then(function (payload) { // Submit payload.nonce to your server }); }, onCancel: function () { // handle case where user cancels }, onError: function (err) { // handle case where error occurs } }).render('#paypal-button'); }).catch(function (err) { console.error('Error!', err); }); ``` General approach: 1. Instantiate braintree client 2. Using braintree client, instantiate payment method 3. Bind to click event (procedural) 4. Call `tokenize` on method to convert to nonce 5. Send to server 6. Capture payment Paypal Approach: 1. Instantiate BT client 2. Load SDK on page 3. Instantiate paypal as payment method (including bind)