MY WORKING SOLUTION for full v2 SDK script with onError, onCancel, and onApprove

matthewperosi
Contributor
Contributor

I have been working on a v2 upgrade in my website software for 3 weeks during the quarantine of the world.  I spent a lot of time banging my head against a wall and decided to share my final running code for everyone interested. 

 

I spent a lot of time studying the PayPal docs and I will agree with many of the other cries for help on this support forum that say how horrible the docs are. I didn't expect to become an expert in JSON over the last few weeks and I didn't think I would have to polish my JavaScript skills to get this done.

 

I'm providing my fully working code with a lot of commentary for everyone.  Delete the comments to clean it up before using it.

 

I HAVE TO POST THIS IN PARTS BECAUSE MY COMMENTARY IS MORE THAN 20,000 CHARACTERS *facepalm*

 

I'm new to the PayPal developer community, and this is my first post.  Give me a vote if this helps you.

 

 

<script src="https://www.paypal.com/sdk/js?client-id=[your-client-id-here]&commit=true&currency=USD"></script>

<script>
// Render the PayPal button into #paypal-button-container
paypal.Buttons({
  style: {
  //style as you prefer
    color:  "gold",
    shape:  "pill",
    size: "large",
    label: "checkout"
  },
// Set up the transaction
createOrder: function(data, actions) {
  return actions.order.create(
  {
    application_context: { 
      shipping_preference: "SET_PROVIDED_ADDRESS",  //Using SET_PROVIDED_ADDRESS will force the shipping address to be grabbed from the purchase_unit below.  This setting also prevents the buyer from changing their address.  Point of note: This setting also seems to disable the PICKUP and SHIPPING options that are normally available. 
      landing_page: "LOGIN", //my preference
      user_action: "PAY_NOW", //my preference
      payment_method: { payer_selected: "PAYPAL", payee_preferred: "IMMEDIATE_PAYMENT_REQUIRED"}
    },
    payer: {
    //I am collecting the payer info on my website and populating it here
    //Point of order: this information is not sent back to you
    //but it seems like this information will help populate the PayPal screens when the buyer doesn't yet have a PayPal account.
    //This invormation is pretty useless if the buyer already has a PayPal account.  PayPal does not use the name or address
    //information when the buyer logs into their account. It seems like this information is simply discarded for buyers
    //with existing PayPal accounts
      name: { given_name: "Test", surname:"Buyer"},
        address: {
          address_line_1: "1 Main St",
          address_line_2: "Apt 1",
          admin_area_2: "Town name",
          admin_area_1: "NJ",
          postal_code: "00000",
          country_code: "US" //You only get the country_code sent back to you
        },
        email_address: "[billing_email_address]", //This email will appear on the PayPal login screen because I specified LOGIN in the above setting
        payment_method: "PAYPAL",
    },

 

 

 

 

 

 

 

Login to Me Too
1 ACCEPTED SOLUTION

Accepted Solutions
Solved

matthewperosi
Contributor
Contributor

PART 2 OF MY CODE

 

    purchase_units: [ 
    //Point of note: purchase_units is an array, you can have more than one when you repeat the structure insite the {}
    //but using more than one seems useless since PayPal only recognizes the shipping address from the first purchase_unit
    //When using more than 1 purchase_unit PayPal will summ the total from all the purchase_units and show that total
    //on the PayPal shadowbox popup. Normally the buyer can click that total to see the item list of everything they are buying,
    //but when you have more than 1 purchase_unit it does not allow the buyer to expand that item list
    //The beauty of using more than 1 purchase_unit is that the buyer and seller will see 2 invoices in their PayPal acounts,
    //which is exactly what I wanted to do, but then I ran into shipping address problems that I document below
    //Another intereting point of note: When using multiple purchase_units you will find that the order is finalized and captured,
    //in other words, you can't authorize the payment then go back later.
    {
      reference_id: "0",  //make sure to always use a ref id, hopefully some day the ability to have multip purcahse_units will actually work
      invoice_id: "1234567890123456961",  //for your own sanity, pass the invoice number from your website. It will make future tracking easier for you
      amount: { currency_code: "USD", value: "278.00",  // this is the total of the breakdown lines below
        //make sure to specify your currency_code and the price, and make sure the price has 2 digits after the decimal
        breakdown: {
          item_total: { currency_code: "USD", value: "280.00"},
          shipping: { currency_code: "USD", value: "0.00"},
          handling: { currency_code: "USD", value: "0.00"},
          tax_total: { currency_code: "USD", value: "0.00"},
          //enter the discount as a positive number.
          discount: { currency_code: "USD", value: "2.00"}
        }
      },
      //write yourself a loop in your code to list all the items
      //I have read through all the PayPal doc regarding chargebacks and I feel that you have
      //bigger protection when you send the item details to PayPal so they appear within the PayPal account.
      //This will also help your own sanity since you can look in your seller account and fully see the 
      //details of your online sales.  Naturally these same details should also be on your website
      items: [  
        {
        name: "Item 1",  //This appears on the PayPal invoice
        //Although the description doesn't really seem to be needed, I found that it also appears on the PayPal invoice. 
        //Notice I am using this to tell the buyer when they are expected to pick the item up.
        //My website allows the customer to choos a pickup or delivery, I tried to follow the docs that say to use 
        //shipping_type:PICKUP, but no matter how hard I tried I could not get that to work, instead, I'm using
        //the description field here to remind the buyer that they have requested to pick this item up.
        //I also give them instructions for proving their ID upon pickup.  
        //This description field also appears on the PayPal payment shadowbox screen as well as on their PayPal invoice
        description: "Store Pickup on 12/31/2020. Bring receipt and photo ID for proof of purchase.",  
        sku: "xyz-2654",
        unit_amount: { currency_code: "USD", value: "100.00"},
        quantity: "1",
        tax: { currency_code: "USD", value: "0.00"},
        category: "PHYSICAL_GOODS"  //I cannot find any place that this is being recorded, but I'm using it anyway to for CYA purposes
        }, //Remember to include this comma in your loop when generating this code on your site.
        {
        name: "Item 2",
        description: "Store Pickup on 12/31/2020. Bring receipt and photo ID for proof of purchase.",
        sku: "zdc-3942",
        unit_amount: { currency_code: "USD", value: "60.00"},
        quantity: "2",
        tax: { currency_code: "USD", value: "0.00"},
        category: "PHYSICAL_GOODS"
        },
        {
        name: "Item 3",
        //In this description I am including the expected shippment date or the expected delivery date, whichever one I have
        description: "Expected shipment on 05/10/2020.",
        sku: "xyz-2654",
        unit_amount: { currency_code: "USD", value: "60.00"},
        quantity: "1",
        tax: { currency_code: "USD", value: "0.00"},
        //I attempted to use DIGITAL_GOODS several times to see if I would get a reply that this item
        //is not covered by seller protection, but I never got any such message. Perhaps PayPal is not correctly
        //generating those messages
        category: "DIGITAL_GOODS"  
        }
      ],

View solution in original post

Login to Me Too
9 REPLIES 9
Solved

matthewperosi
Contributor
Contributor

PART 2 OF MY CODE

 

    purchase_units: [ 
    //Point of note: purchase_units is an array, you can have more than one when you repeat the structure insite the {}
    //but using more than one seems useless since PayPal only recognizes the shipping address from the first purchase_unit
    //When using more than 1 purchase_unit PayPal will summ the total from all the purchase_units and show that total
    //on the PayPal shadowbox popup. Normally the buyer can click that total to see the item list of everything they are buying,
    //but when you have more than 1 purchase_unit it does not allow the buyer to expand that item list
    //The beauty of using more than 1 purchase_unit is that the buyer and seller will see 2 invoices in their PayPal acounts,
    //which is exactly what I wanted to do, but then I ran into shipping address problems that I document below
    //Another intereting point of note: When using multiple purchase_units you will find that the order is finalized and captured,
    //in other words, you can't authorize the payment then go back later.
    {
      reference_id: "0",  //make sure to always use a ref id, hopefully some day the ability to have multip purcahse_units will actually work
      invoice_id: "1234567890123456961",  //for your own sanity, pass the invoice number from your website. It will make future tracking easier for you
      amount: { currency_code: "USD", value: "278.00",  // this is the total of the breakdown lines below
        //make sure to specify your currency_code and the price, and make sure the price has 2 digits after the decimal
        breakdown: {
          item_total: { currency_code: "USD", value: "280.00"},
          shipping: { currency_code: "USD", value: "0.00"},
          handling: { currency_code: "USD", value: "0.00"},
          tax_total: { currency_code: "USD", value: "0.00"},
          //enter the discount as a positive number.
          discount: { currency_code: "USD", value: "2.00"}
        }
      },
      //write yourself a loop in your code to list all the items
      //I have read through all the PayPal doc regarding chargebacks and I feel that you have
      //bigger protection when you send the item details to PayPal so they appear within the PayPal account.
      //This will also help your own sanity since you can look in your seller account and fully see the 
      //details of your online sales.  Naturally these same details should also be on your website
      items: [  
        {
        name: "Item 1",  //This appears on the PayPal invoice
        //Although the description doesn't really seem to be needed, I found that it also appears on the PayPal invoice. 
        //Notice I am using this to tell the buyer when they are expected to pick the item up.
        //My website allows the customer to choos a pickup or delivery, I tried to follow the docs that say to use 
        //shipping_type:PICKUP, but no matter how hard I tried I could not get that to work, instead, I'm using
        //the description field here to remind the buyer that they have requested to pick this item up.
        //I also give them instructions for proving their ID upon pickup.  
        //This description field also appears on the PayPal payment shadowbox screen as well as on their PayPal invoice
        description: "Store Pickup on 12/31/2020. Bring receipt and photo ID for proof of purchase.",  
        sku: "xyz-2654",
        unit_amount: { currency_code: "USD", value: "100.00"},
        quantity: "1",
        tax: { currency_code: "USD", value: "0.00"},
        category: "PHYSICAL_GOODS"  //I cannot find any place that this is being recorded, but I'm using it anyway to for CYA purposes
        }, //Remember to include this comma in your loop when generating this code on your site.
        {
        name: "Item 2",
        description: "Store Pickup on 12/31/2020. Bring receipt and photo ID for proof of purchase.",
        sku: "zdc-3942",
        unit_amount: { currency_code: "USD", value: "60.00"},
        quantity: "2",
        tax: { currency_code: "USD", value: "0.00"},
        category: "PHYSICAL_GOODS"
        },
        {
        name: "Item 3",
        //In this description I am including the expected shippment date or the expected delivery date, whichever one I have
        description: "Expected shipment on 05/10/2020.",
        sku: "xyz-2654",
        unit_amount: { currency_code: "USD", value: "60.00"},
        quantity: "1",
        tax: { currency_code: "USD", value: "0.00"},
        //I attempted to use DIGITAL_GOODS several times to see if I would get a reply that this item
        //is not covered by seller protection, but I never got any such message. Perhaps PayPal is not correctly
        //generating those messages
        category: "DIGITAL_GOODS"  
        }
      ],
Login to Me Too

matthewperosi
Contributor
Contributor

PART 3 of my Code

 

 

      //As I said above, when using more than 1 purchase_unit PayPal will create 1 invoice per purchase_unit.
      //Sadly, I found that PayPal will ONLY use the shipping address from the FIRST purchase_unit for all of the invoices
      //In one of the docs I found a note saying that you should not sell PICKUP and SHIPPING items in the same transaction
      //because the PICKUP items do not have seller protection but the SHIPPING items do.
      //It's a little ridiculious to ask a buyer to go through multiple checkout transactions when they want to both
      //pick something up and also have something shipped to them.  PayPal also denies seller protection for DIGITAL_GOODS
      //which also seems to mean that you should not sell DIGITAL_GOODS and PHYSICAL_GOODS on the same transaction,
      //again, this is another great oppertunity to use multiple purchase_units; but again, I could not get this to work
      //The customer is entering their shipping address on my website.  I let them enter a different billing and shipping 
      //address, then I show that shipping address here.  Because I am using SET_PROVIDED_ADDRESS above, they are not able
      //to change this address on PayPal.  This give me more seller protection because now I can control the shipping address.
      //When the buyer has the power to change their shipping address on PayPal, you have to grab that new address for your
      //records.  Since I've already captured their address and calculated shipping on my website I don't want them to change
      //the address on PayPal.  I also don't want them to be sneaky **bleep** and change the address without me knowing
      //which would ultimatly allow them to claim they never got the shippment to their specified address, and win a chargeback claim
      shipping:{
        address: {
          //The address you send to PayPal here will appear on the PayPal invoice.  This is the address you must ship to
          //in order to have your full seller protection.
          //When I was trying multiple purchase_units only the address from the first purchase_unit was grabbed for all the
          //invoices.  I was trying to have one purchase unit for pickup items, one for shipping items, and one for digital downloads.
          //I also wanted to have additional purchase_units for items that were grouped together with different shipping dates.
          //But since PayPal does not recognize the different addresses in each purchase_unit the whole concept of those 
          //multiple invoices with different seller protections went out the window. 
          //When someone requests to have their entire order picked up at the store, I set this address to the store address
          address_line_1: "2 Broadway Blvd",
          address_line_2: "Apt 1",
          admin_area_2: "Sometown",
          admin_area_1: "NJ",
          postal_code: "00000",
          country_code: "US"
        },
        //As I said above, the PayPal doesn't seem to care about the payer.name or payer.address invormation that I send above
        //however, the name shown below will appear on the invoice as the ship-to person
        name: { full_name: "Ref 1 Buyer"}
      }
		}]
	});
},

 

Login to Me Too

matthewperosi
Contributor
Contributor

PART 4 of my Code

 

 

//It took me 5 days to figure out everything above, then another 5 days to figure out this sectioin below
//The PayPal docs for v2 are so messed up with regard to the onApprove, onCancel, and onError
//I figured the following out after reading the pain and suffering from dozens of other developers 
//trying to accomplish this for the last 2 years.
//Point of note: The onError suggestion in the docs seems useless.  I have a feeling there is more to it
//than what is documented, but the docs are limited.  Why would we, as developers, want to simply show an error
//message to the buyer instead of an actual error code that we can interpret and make look pretty?
//I also found that there seemed to be no way to grab the orderID from within the onError function, which 
//make the entire onError function seem pointless because you then have now way to even track the attempted payment.
//Therefore, I am using the onAuthorize to capture both the successfule and unsuccessful payment attempts
onApprove: function(data, actions) {
  return actions.order.capture().then(function (details) {
    //All of the alerts you see are for testing purposes only.  When your website is fully functional, you
    //should not rely on these alerts.  A better user experience is to grab the results and have your website react accordingly
    //This alert is just here to show me were I am in the capture process
    alert('Transaction completed by ' + details.payer.name.given_name);
    //I create a variable for the URL I'm using to save the transaction results
    var EXECUTE_URL = '[paypalsave]?mycart=[mycart]&hash=[hash]&sd=[numseed]';
    //Initially I didn't like the idea of fetch, I preferred to either redirect directly like we could in v1 of checkout.js
    //but that ability to redirect and capture the JSON result seems to be gone now.  IMHO that is stupid and sad.
    //Now we have to grab the JSON and save it before redirecting the buyer to the final page of the site.
    fetch(EXECUTE_URL, {
      method: 'post',
      headers: {
      'content-type': 'application/json' 
      },
      body: JSON.stringify({
        orderID: data.orderID,  //This variable is suggested in the docs
        payerID: data.payerID,  //This variable is also suggested in the docs
        //I also tried to grab data.status and save that, but 'status' is a reserved word and therefore it can't be easily grabbed from the JSON
        amtpaid: details.purchase_units[0].amount.value,   //I'm grabbing this to speed up the processing steps on my end
        invoice_id: details.purchase_units[0].invoice_id,  //I'm grabbing this to cross refrence my own shopping cart variable "mycart" to the invoice_id. They should be the same
        finalstate: details.purchase_units[0].payments.captures[0].final_capture  //This will be set to "true" if the payment was successful.  I'm using this in place of my attempt to grab data.status as mentioned above
      })
    //Here's where things get interesting.  I now do a test to make sure the fetch actually worked.  
    //This is where I test if the network error broke the fetch.  
    }).then (response => {
        //If the fetch response if NOT ok, then I am goint to try doing a redirect to my next screen with URL variables that will pass along the state 
        //as well as a "fetcherror" variable to tell me the fetch broke
        //I thought about skipping the fetch all together and just redirecting to the next page with the URL and the GET, but then I 
        //realized that more information could be sent in the fetch, and since it's a POST, there's a little more security.
        //I'm also planning for future expansion when I can grab more information from the PayPal results and fetch them over to safety
        if (!response.ok) {
          //I'm building me redirect URL here
          var uri = "[paypalreturn]&fetcherror=T&orderid="+data.orderID+"&payerID="+data.payerID+"&finalstate="+details.purchase_units[0].payments.captures[0].final_capture;
          //Since the redirect is a GET, the URL needs to be encoded
          var res = encodeURI(uri); 
          //The console message is used in testing.  I had the location.href commented out during testing so I could see the console errors in DevTools of my browser
          console.log('res',res);
          //When redirecting at this point I already know that the payment was successful and teh rest of the script below doesn't matter to me any more
          //I'm actually redirecting the buyer to the thank you page right here, but the extra added URL variables will tell my thank you page that there
          //was something wrong with the fetch to save the JSON, and it will process the results differently
          location.href=res;
        }
    });
    //If the script gets to this point I know that the fetch above was successful.  If the fetch wasn't successful I would have already been redirected away from this page
    //I'm showing myself messages in the colsole only for testing.  In production I comment the colsole lines out or just delete them
    console.log('given_name'); console.log(details.payer.name.given_name); 
    console.log('return to [paypalreturn]'); 
    //This is where I would notmally redirect the buyer to the thank you page
    //Under normal conditions I do not have to send any GET variables to the thank you page.  The fetch saved JSON will be waiting for me over there
    location.href = '[paypalreturn]';
  })
  //And now we handle the errors.  This is how I capture errors and display usable mesages that I could not get from onError as suggested in the docs.
  //This catch will recognize when the return actions.order.capture() above fails
  .catch(error => {
    //The alert is just for my own testing.  It is commented out or deleted in production mode
    alert('error inside approve ');
    //These console messages are also for testing. They are commented out or deleted in production mode
    console.log ('error inside approve');
    console.log ('orderID',data.orderID);
    console.log ('payerID',data.payerID);
    //Similar to what I did above, this is just prepwork for my fetch URL
    var EXECUTE_URL = '[paypalsave]?mycart=[mycart]&hash=[hash]&sd=[numseed]';
    fetch(EXECUTE_URL, {
      method: 'post',
      headers: {
        'content-type': 'application/json' 
      },
      //As I studied the PayPal v2 SDK docs I could not figure out how to grab any usable results from the onError.
      //specifically, I needed to orderID so I could track the failed order and help a buyer.
      //I saw several other people on this forum request a way to get order details and at least one PayPal Emploee tell them to use the onError function
      //I really felt like someone was singing "there's a hole in the bucket, dear Liza, dear Liza" and no one was paying attention
      //By including the error capture within the onAuthorize I found that the order variable are still available
      //That's exactly why this is here, and you'll notice that I removed the onError from my script
      body: JSON.stringify({
        orderID: data.orderID,
        payerID: data.payerID,
        capturestatus:'ERROR'  //This is my identifier that there's a problem.  I can use the orderID on the next screen to lookup the order and figure out what went wrong
      })
    //Similar to above, this next part is protection against a potential network error during the fetch.
    }).then (response => {
      //Same as before, if the fetch result is NOT ok, then we will prepare a GET and redirect to the next page
      if (!response.ok) {
      //Prepping my next URL, it has the same info as the fetch.
      var uri = "[paypalreturn]&fetcherror=T&orderid="+data.orderID+"&payerID="+data.payerID+"&capturestatus=ERROR";
      //Once again, since this is a URL I need to encode the URI or spacing and other characters might break it
      var res = encodeURI(uri); 
      //The console message is only for testing.  I had the location.href commented out for testing.
      //The consol line will be deleted or commented out for production
      console.log('res',res);
      location.href=res;
      }
    });
    //If the fetch worked correctly, then this is where I expect to be.
    //The console line is only for testing.  It iw commented out or deleted for production
    console.log('return to [paypalreturn]'); 
    //The location.href was commented out for testing.  This is where I jumpt the buyer to the next page
    //My next page is actually my thank you page, but when I get there I'll find the fetch JSON telling me that there was an error
    //Instead of showing the thank you I will use the data.orderID that I was able to save then ping PayPal to find out what the error was
    //I built an error table on my website to translate the PayPal errors into something understadable by the buyer.
    //I also built an email notification feature into my error routine that tells the seller that there is a purchase error along with all the details from PayPal
    location.href = '[paypalreturn]';
  });
},
//The onCancel works as expected.  Perhaps the only part of this entire v2 SDK that worked without a single problem
onCancel: function(data) {
    location.href='[paypalcancel]';
},
}).render('#paypal-button-container');
</script>

 

Login to Me Too

lunardigital
Contributor
Contributor

You're a legend mate. They really need to put out a full Javascript SDK documentation...or else they shouldn't even direct people to the silly buttons guide...

Login to Me Too

matthewperosi
Contributor
Contributor

Thanks! 😀

Login to Me Too

adardesign
New Community Member

Very helpful, thanks 

Please find this gist (I only copied it into one page)

https://gist.github.com/adardesign/96e5805d6559e594bde46fa555367e69

Login to Me Too

maxminhtran
Contributor
Contributor

Your script @adardesign contains some codes that is repeated from a double post of Matthew. You may want to edit the script.

Login to Me Too

maxminhtran
Contributor
Contributor

Thank You, @matthewperosi Matthew! Your posts were very useful. I could not get your script to work for my magazine subscription demo, so I modified and combined your code with some code found else where.

Login to Me Too

maxminhtran
Contributor
Contributor

To give back to the community the many useful codes and explanation that I received, especially the posts of Matthew Perosi, I have created a complete demo using PHP and a modified version of the SDK code, combined with others found else where. The link to the demo is PHP and V2 SDK demo of a magazine subscription framework.

Inside the zip file is a word document explain the system in more details.

Login to Me Too

Haven't Found your Answer?

It happens. Hit the "Login to Ask the community" button to create a question for the PayPal community.