3
votes

This SO answer correctly explains that since the require Node/JS library is not supported by Google Apps Script, the following code changes must be made to get Stripe to work properly in a GAS project:

from
const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
(async () => {
  const product = await stripe.products.create({
    name: 'My SaaS Platform',
    type: 'service',
  });
})();
to
function myFunction() {
  var url = "https://api.stripe.com/v1/products";
  var params = {
    method: "post",
    headers: {Authorization: "Basic " + Utilities.base64Encode("sk_test_4eC39HqLyjWDarjtT1zdp7dc:")},
    payload: {name: "My SaaS Platform", type: "service"}
  };
  var res = UrlFetchApp.fetch(url, params);
  Logger.log(res.getContentText())
}

Now, I want to convert the following code into the Google Apps Script friendly version.

from https://stripe.com/docs/payments/checkout/accept-a-payment#create-checkout-session
const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card', 'ideal'],
  line_items: [{
    price_data: {
      currency: 'eur',
      product_data: {
        name: 'T-shirt',
      },
      unit_amount: 2000,
    },
    quantity: 1,
  }],
  mode: 'payment',
  success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
  cancel_url: 'https://example.com/cancel',
});

So, I'm trying the following.

to
function myFunction() {
  var url = "https://api.stripe.com/v1/checkout/sessions";
  var params = {
    method: "post",
    headers: {
      Authorization:
        "Basic " + Utilities.base64Encode("sk_test_4eC39HqLyjWDarjtT1zdp7dc:"),
    },
    payload: {
      payment_method_types: ["card", "ideal"],
      line_items: [
        {
          price_data: {
            currency: "eur",
            product_data: {
              name: "T-shirt",
            },
            unit_amount: 2000,
          },
          quantity: 1,
        },
      ],
      mode: "payment",
      success_url:
        "https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
      cancel_url: "https://example.com/cancel",
    },
  };
  var res = UrlFetchApp.fetch(url, params);
  Logger.log(res.getContentText());
}

However, instead of getting the expected response object returned, I get the following error:

Exception: Request failed for https://api.stripe.com returned code 400. Truncated server response:

Log.error
{
  error: {
    message: "Invalid array",
    param: "line_items",
    type: "invalid_request_error",
  },
}

What am I doing wrong? And how can I generalize the first example? What is the specific documentation I need that I'm not seeing?

Edit:

After stringifying the payload per the suggestion from the comments, now I get the following error:

Exception: Request failed for https://api.stripe.com returned code 400. Truncated server response:

Log.error
{
  "error": {
    "code": "parameter_unknown",
    "doc_url": "https://stripe.com/docs/error-codes/parameter-unknown",
    "message": "Received unknown parameter: {\"payment_method_types\":. Did you mean payment_method_types?",
    "param": "{\"payment_method_types\":",
    "type": "invalid_request_error"
  }
}
2
Stringify the payload - TheMaster
Link the official page of the documentation you're referring to - TheMaster
@TheMaster: When I stringify the payload, I get a different error — as described in the edit. That error suggests the stringification is creating the problem. That error is shown in the log and in a popup window of the script editor so there is no link available to a URL. Also, in the first example, there was no need to stringify the payload as the first example produced the expected behavior by returning the requested object from the server. You can test in your script editor with a simple copy and paste. No special setup is required. The API key is public for testing purposes. - Let Me Tink About It
I mean the page you got sample. Documentation of stripe-api - TheMaster
@TheMaster: Oh, sorry, here's the second one of them. I also edited the question to include it. stripe.com/docs/payments/checkout/… I'll look for the other and add it too. - Let Me Tink About It

2 Answers

1
votes

From this question and this sample script, I thought that in this case, the values are required to be sent as the form data. So how about the following modification?

Modified script:

function myFunction() {
  var url = "https://httpbin.org/anything";
  var params = {
    method: "post",
    headers: {Authorization: "Basic " + Utilities.base64Encode("sk_test_4eC39HqLyjWDarjtT1zdp7dc:")},
    payload: {
      "cancel_url": "https://example.com/cancel",
      "line_items[0][price_data][currency]": "eur",
      "line_items[0][price_data][product_data][name]": "T-shirt",
      "line_items[0][price_data][unit_amount]": "2000",
      "line_items[0][quantity]": "1",
      "mode": "payment",
      "payment_method_types[0]": "card",
      "payment_method_types[1]": "ideal",
      "success_url": "https://example.com/success?session_id={CHECKOUT_SESSION_ID}"
    }
  };
  var res = UrlFetchApp.fetch(url, params);
  Logger.log(res.getContentText());  // or console.log(res.getContentText())
}
  • I think that point might be payment_method_types: ["card", "ideal"] is required to be sent as "payment_method_types[0]": "card" and "payment_method_types[1]": "ideal".

References:

1
votes

I modified the script mentioned in the comments and now have a one that works for this purpose.

// [ BEGIN ] utilities library

/**
 * Returns encoded form suitable for HTTP POST: x-www-urlformencoded
 * @see code: https://gist.github.com/lastguest/1fd181a9c9db0550a847
 * @see context: https://stackoverflow.com/a/63024022
 * @param { Object } element a data object needing to be encoded
 * @param { String } key not necessary for initial call, but used for recursive call
 * @param { Object } result recursively populated return object
 * @returns { Object } a stringified object
 * @example
 *  `{
        "cancel_url": "https://example.com/cancel",
        "line_items[0][price_data][currency]": "eur",
        "line_items[0][price_data][product_data][name]": "T-shirt",
        "line_items[0][price_data][unit_amount]": "2000",
        "line_items[0][quantity]": "1",
        "mode": "payment",
        "payment_method_types[0]": "card",
        "payment_method_types[1]": "ideal",
        "success_url": "https://example.com/success?session_id={CHECKOUT_SESSION_ID}"
      }`
 */
const json2urlEncoded = ( element, key, result={}, ) => {
  const OBJECT = 'object';
  const typeOfElement = typeof( element );
  if( typeOfElement === OBJECT ){
    for ( const index in element ) {
      const elementParam = element[ index ];
      const keyParam = key ? `${ key }[${ index }]` : index;
      json2urlEncoded( elementParam, keyParam, result, );
    }
  } else {
    result[ key ] = element.toString();
  }
  return result;
}
// // test
// const json2urlEncoded_test = () => {
//   const data = {
//     time : +new Date,
//     users : [
//       { id: 100 , name: 'Alice'   , } ,
//       { id: 200 , name: 'Bob'     , } ,
//       { id: 300 , name: 'Charlie' , } ,
//     ],  
//   };
//   const test = json2urlEncoded( data, );
//   // Logger.log( 'test\n%s', test, );
//   return test;
//   // Output:
//   // users[0][id]=100&users[0][name]=Stefano&users[1][id]=200&users[1][name]=Lucia&users[2][id]=300&users[2][name]=Franco&time=1405014230183
// }
// // quokka
// const test = json2urlEncoded_test();
// const typeOfTest = typeof test;
// typeOfTest
// test

// [ END ] utilities library