<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Corentin</title>
    <description>The latest articles on DEV Community by Corentin (@frenchcooc).</description>
    <link>https://dev.to/frenchcooc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F63197%2F85133974-61c0-4b33-b860-9534ebd62ff3.jpg</url>
      <title>DEV Community: Corentin</title>
      <link>https://dev.to/frenchcooc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/frenchcooc"/>
    <language>en</language>
    <item>
      <title>🚀 Introducing EmbedLite. A Drop-In, Privacy-Friendly Replacement for YouTube Embeds</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Fri, 03 Oct 2025 09:30:58 +0000</pubDate>
      <link>https://dev.to/frenchcooc/introducing-embedlite-a-drop-in-privacy-friendly-replacement-for-youtube-embeds-jc5</link>
      <guid>https://dev.to/frenchcooc/introducing-embedlite-a-drop-in-privacy-friendly-replacement-for-youtube-embeds-jc5</guid>
      <description>&lt;p&gt;YouTube’s embed iframe is kind of… awful. Every time you paste a YouTube &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; into your website, it loads &lt;strong&gt;hundreds of kilobytes of scripts&lt;/strong&gt;, and slows down your page. Even if your visitor never clicks “play.”&lt;/p&gt;

&lt;p&gt;So I built something to fix that.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://embedlite.com" rel="noopener noreferrer"&gt;EmbedLite.com&lt;/a&gt;&lt;/strong&gt; — a drop-in, open-source replacement for YouTube’s embed iframe that’s &lt;strong&gt;lightning fast, SEO-optimized, and privacy-friendly&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 The Problem
&lt;/h2&gt;

&lt;p&gt;If you’ve ever run Lighthouse or PageSpeed Insights on a page with multiple YouTube videos, you’ve probably noticed how bad it gets.&lt;/p&gt;

&lt;p&gt;A single YouTube embed can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load multiple JavaScript bundles from &lt;code&gt;youtube.com&lt;/code&gt;, &lt;code&gt;google.com&lt;/code&gt;, and &lt;code&gt;doubleclick.net&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add network requests even before interaction&lt;/li&gt;
&lt;li&gt;Delay Largest Contentful Paint (LCP)&lt;/li&gt;
&lt;li&gt;Leak privacy data through tracking pixels and cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's for each video on the page...&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ The Solution: EmbedLite
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://embedlite.com" rel="noopener noreferrer"&gt;EmbedLite&lt;/a&gt; replaces the heavy YouTube iframe with a &lt;strong&gt;tiny, static placeholder&lt;/strong&gt; — basically a thumbnail and play button.  The real YouTube player only loads after the user clicks “play.”&lt;/p&gt;

&lt;p&gt;That’s it. No tracking, no preloading, no layout shifts.&lt;/p&gt;

&lt;p&gt;All you have to do is change this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-- &amp;lt;iframe src="https://youtube.com/embed/..."
&lt;/span&gt;&lt;span class="gi"&gt;++ &amp;lt;iframe src="https://embedlite.com/embed/..."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you’re done.&lt;/p&gt;

&lt;p&gt;Your site instantly becomes faster, more privacy-friendly, and better optimized for SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 How It Works
&lt;/h2&gt;

&lt;p&gt;Under the hood, EmbedLite is a super-light static app. It fetches the video thumbnail and title, displays them with minimal HTML and CSS, and only when someone interacts does it swap in the real YouTube iframe.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero external scripts&lt;/strong&gt; until play
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cookies&lt;/strong&gt; or tracking before user consent
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller total payloads&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant page loads&lt;/strong&gt;, even with multiple videos
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s inspired by projects like &lt;a href="https://github.com/paulirish/lite-youtube-embed" rel="noopener noreferrer"&gt;Paul Irish’s lite-youtube-embed&lt;/a&gt; and &lt;a href="https://github.com/justinribeiro/lite-youtube" rel="noopener noreferrer"&gt;Justin Ribeiro’s lite-youtube&lt;/a&gt;, but it’s &lt;strong&gt;hosted, API-like, and even simpler to use.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ Usage
&lt;/h2&gt;

&lt;p&gt;Replace your YouTube embed with an EmbedLite URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-- &amp;lt;iframe src="https://www.youtube.com/embed/jNQXAC9IVRw" ...&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;++ &amp;lt;iframe src="https://www.embedlite.com/embed/jNQXAC9IVRw" ...&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s literally it.&lt;/p&gt;

&lt;p&gt;You can also pass parameters just like you would to YouTube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;https://embedlite.com/embed/dQw4w9WgXcQ?controls&lt;span class="o"&gt;=&lt;/span&gt;0&amp;amp;mute&lt;span class="o"&gt;=&lt;/span&gt;1&amp;amp;start&lt;span class="o"&gt;=&lt;/span&gt;30&amp;amp;end&lt;span class="o"&gt;=&lt;/span&gt;120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set a custom title:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;https://embedlite.com/embed/dQw4w9WgXcQ?autoplay&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&amp;amp;title&lt;span class="o"&gt;=&lt;/span&gt;Rick%20Astley
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ☁️ Host It Yourself (If You Want)
&lt;/h2&gt;

&lt;p&gt;You can deploy your own version of EmbedLite anywhere static files can live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Or your own server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is open source on GitHub: &lt;a href="https://github.com/embedlite/embedlite" rel="noopener noreferrer"&gt;github.com/embedlite/embedlite&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Why This Matters
&lt;/h2&gt;

&lt;p&gt;By reducing unnecessary network requests and avoiding trackers, EmbedLite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improves &lt;strong&gt;Core Web Vitals&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Keeps &lt;strong&gt;privacy&lt;/strong&gt; in the user’s hands&lt;/li&gt;
&lt;li&gt;Makes your &lt;strong&gt;frontend simpler&lt;/strong&gt; and lighter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a small fix with a surprisingly big impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧑‍💻 What's next?
&lt;/h3&gt;

&lt;p&gt;I built EmbedLite because I was tired of my pages getting bogged down by YouTube embeds. I plan on making a npm package so that it's easy to add to your project. Let me know your thoughts!&lt;/p&gt;

&lt;p&gt;In the meantime, give it a try at:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://embedlite.com" rel="noopener noreferrer"&gt;EmbedLite.com&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
📦 &lt;strong&gt;&lt;a href="https://github.com/embedlite/embedlite" rel="noopener noreferrer"&gt;GitHub: embedlite/embedlite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>news</category>
    </item>
    <item>
      <title>YouTube embed</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Wed, 30 Jul 2025 17:32:18 +0000</pubDate>
      <link>https://dev.to/frenchcooc/youtube-embed-350e</link>
      <guid>https://dev.to/frenchcooc/youtube-embed-350e</guid>
      <description></description>
      <category>embeds</category>
    </item>
    <item>
      <title>Migrating from Google's reCAPTCHA to Cloudflare Turnstile?</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Mon, 22 Jul 2024 15:43:08 +0000</pubDate>
      <link>https://dev.to/frenchcooc/migrating-from-googles-recaptcha-to-cloudflare-turnstile-17lf</link>
      <guid>https://dev.to/frenchcooc/migrating-from-googles-recaptcha-to-cloudflare-turnstile-17lf</guid>
      <description>&lt;p&gt;&lt;a href="https://cloud.google.com/security/products/recaptcha?hl=en#pricing" rel="noopener noreferrer"&gt;Google reCAPTCHA new pricing&lt;/a&gt; will be rolled out on August 1st, meaning that you have a few days left to migrate to a cheaper alternative or ensure your bank account is well-funded. &lt;/p&gt;

&lt;p&gt;Starting at $1 for 1,000 verifications, it is going to cost a lot. At Mailmeteor, we use reCAPTCHA extensively to protect our services from bots. With Google's pricing change, we calculated that we're about to pay thousands of dollar per month to keep using their reCAPTCHA service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a CAPTCHA?
&lt;/h2&gt;

&lt;p&gt;CAPTCHAs are an essential part of the web. It aims to separate good citizens from bad actors. Essentially, it's a service that will operate on the frontend and generate a token that is transmitted to the backend. The backend then verifies that the token is legit, and, if so, performs the action.&lt;/p&gt;

&lt;p&gt;Google did a great job at promoting their own service, but thankfully, there are some alternatives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.hcaptcha.com/" rel="noopener noreferrer"&gt;hCaptcha&lt;/a&gt;. We considered it at first, but their pricing is quite similar to new Google's pricing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cloudflare.com/products/turnstile/" rel="noopener noreferrer"&gt;Cloudflare Turnstile&lt;/a&gt;. We are huge fans of Cloudflare, and definitely looked into it. As for now, it's a free service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's dig in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving away from Google reCAPTCHA...
&lt;/h2&gt;

&lt;p&gt;One of our free tools is an &lt;a href="https://mailmeteor.com/tools/ai-email-writer" rel="noopener noreferrer"&gt;AI Email Writer&lt;/a&gt;. It's basically an HTML page that send request to our backend, which then makes to a third-party AI solution.&lt;/p&gt;

&lt;p&gt;To protect it from abuse, Google reCAPTCHA was enabled from day one. Here's how the verification was done so far (backend-side):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.js&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/email-ai-writer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aiEmailWriter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ai_email_writer.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aiEmailWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Recaptcha&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: verification failed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: verification failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aiemailwriter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: bad action name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: bad action name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Recaptcha: score is below 0.3 (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: score too low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's quite simple and it's an essential part of why Google reCAPTCHA was so popular. The footprint is very limited and it's really easy to implement. For the most curious ones, we leveraged the &lt;a href="https://www.npmjs.com/package/express-recaptcha" rel="noopener noreferrer"&gt;express-recaptcha&lt;/a&gt; package to make it really easy to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  ... to Cloudflare Turnstile
&lt;/h2&gt;

&lt;p&gt;When migrating to Turnstile, we couldn't found an NPM package, so we had to write a middleware to process the token. Here's how it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middlewares/turnstile.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turnstile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Turnstile injects a token in "cf-turnstile-response".&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CF-Connecting-IP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing CloudFlare Turnstile Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate the token by calling the&lt;/span&gt;
    &lt;span class="c1"&gt;// "/siteverify" API endpoint.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDFLARE_TURNSTILE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remoteip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://challenges.cloudflare.com/turnstile/v0/siteverify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;// Process the verification outcome&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CloudFlare Turnstile declined the token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnstile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;

    &lt;span class="c1"&gt;// If authentified, go to next middleware&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;turnstile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the middleware is in place, we can apply it to any requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// index.js
app.post('/api/ai-email-writer', aiRateLimiter, turnstile, aiEmailWriter)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And inside the function that treats the request, it's quite similar to what we had previously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ai_email_writer.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aiEmailWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// CloudFlare Turnstile protection&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnstile&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnstile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: verification failed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: verification failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnstile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aiemailwriter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: bad action name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recaptcha: bad action name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating from reCAPTCHA to Turnstile is straightforward and shouldn't take more than a few hours. It works quite similar and will definitely save you a lot of money at the same time.&lt;/p&gt;

&lt;p&gt;I didn't cover the frontend in this article, because we use an invisible widget that our users don't see. But &lt;a href="https://developers.cloudflare.com/turnstile/concepts/widget-types/" rel="noopener noreferrer"&gt;Turnstile's documentation&lt;/a&gt; covers extensively how to use their interactive widgets.&lt;/p&gt;

&lt;p&gt;Call it a day!&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>How to monetize your Google Workspace add-on?</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Sun, 06 Nov 2022 17:13:24 +0000</pubDate>
      <link>https://dev.to/frenchcooc/how-to-monetize-your-google-workspace-add-on-1k0a</link>
      <guid>https://dev.to/frenchcooc/how-to-monetize-your-google-workspace-add-on-1k0a</guid>
      <description>&lt;p&gt;You've built a great add-on for the Google Workspace ecosystem (Google Docs, Google Sheets, Gmail &amp;amp; co.). It's getting a lot of traction and you feel like you can monetize it. If this is your current situation, great! We were in the same situation a few years ago at Mailmeteor and we totally understand how excited you can feel today.&lt;/p&gt;

&lt;p&gt;Below is the guide I wish I had found a few years ago when I was researching how to monetize our &lt;a href="https://workspace.google.com/marketplace/app/mailmeteor_mail_merge_for_gmail/1008170693301"&gt;mail merge add-on for Google Sheets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There are several ways to manage how you monetize add-ons. I'll share with you how we do it at Mailmeteor. Our approach is - I believe - one that provides the best user experience (for your users) and high security (for you, the developer).&lt;/p&gt;

&lt;p&gt;Also, at Mailmeteor, we heavily rely on &lt;a href="https://firebase.google.com/"&gt;Firebase&lt;/a&gt;, which has a NoSQL database (Real-time Database) and can run serverless functions (Cloud Functions). We use both to handle the licensing. If you aren't familiar with Firebase yet, feel free to read on. The general concept still applies even if you prefer AWS or other cloud providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Authentication
&lt;/h2&gt;

&lt;p&gt;First, you need to authenticate your users. When you build an add-on, sometimes you don't have a way for your users to authenticate. That's especially true for narrowed add-ons (that provide a limited set of features). The thing is as soon as you want to monetize your add-on, you need to provide a signup flow to safely authenticate users and retrieve their status (e.g. free vs. paid).&lt;/p&gt;

&lt;p&gt;Google Apps Script already provides an authentication mechanism, so there's no need to start from scratch. Using &lt;code&gt;UserSession.getEmail()&lt;/code&gt; you can safely know which user is running your code. That's a very good start. Plus, if you take this path (which we did at Mailmeteor) you don't need to prompt to users an email/password form. The user is already logged in. That's a high-five for your users, plus this will save you hours of work.&lt;/p&gt;

&lt;p&gt;This being said, now you need to make Google Apps Script communicate with your backend. As I said, at Mailmetoer, we use Cloud Functions for our backend. This has the benefit of being relatively cheap at the start and scaling automatically as the add-on grows. But, Cloud Functions is not a requirement, you could grab a cheap $4 droplet on DigitalOcean and that would work just as well.&lt;/p&gt;

&lt;p&gt;To make Google Apps Script work with your backend, you can create a &lt;code&gt;User&lt;/code&gt; class in your Apps Script code. And add a &lt;code&gt;getProfile&lt;/code&gt; method in it. That method will fetch the user profile every time you call the function. Here's a gist of our &lt;code&gt;user.gs&lt;/code&gt; file in Apps Script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUserIdFromEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEffectiveUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;getEmail&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BACKEND_ENDPOINT_&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/user?uid=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;BACKEND_TOKEN_&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getContentText&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;getUserIdFromEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`SOME_RANDOM_STRING-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;computeDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DigestAlgorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHA_256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To retrieve the authenticated user profile, you would run &lt;code&gt;new User().getProfile()&lt;/code&gt;. As you can see, we rely on &lt;code&gt;Session.getEffectiveUser().getEmail()&lt;/code&gt; to safely retrieve the authenticated user email. This method is made available by Google Apps Script via the &lt;a href="https://developers.google.com/apps-script/reference/base/session"&gt;&lt;code&gt;Session&lt;/code&gt; class&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also, you might have noticed that the user's ID is generated by the &lt;code&gt;getUserIdFromEmail&lt;/code&gt; which is just an &lt;code&gt;SHA-256&lt;/code&gt; of the user email. That won't resist brute force, so we prefix it with a random string to make it harder to guess.&lt;/p&gt;

&lt;p&gt;Sometimes, your addon might use Apps Script to &lt;a href="https://developers.google.com/apps-script/guides/html"&gt;create and serve HTML&lt;/a&gt;, for example by showing a modal or sidebar. In such a case, you would have to create a function in your &lt;code&gt;user.gs&lt;/code&gt; file: &lt;code&gt;function getUserProfile() { return new User().getProfile() }&lt;/code&gt;. Then, you can retrieve the user profile from the HTML page by running &lt;code&gt;google.script.run.getUserProfile()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now that you know how to retrieve a user profile, let's see how to create a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. User creation
&lt;/h2&gt;

&lt;p&gt;In your backend, you will need to create the &lt;code&gt;/user&lt;/code&gt; endpoint that will retrieve the user profile. Again, Firebase provides a great environment to do just that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Retrieve params&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Validation&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_params&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Retrieve the profile from Firebase Real-time Database&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FirebaseDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSnap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userSnap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

    &lt;span class="c1"&gt;// Send response to client&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user ID generated in Apps Script. We use it as a key in our Firebase Real-time Database to retrieve the user profile. That's very easy to implement. Before releasing to production, make sure to secure your application by protecting your database from read/write (&lt;a href="https://firebase.google.com/docs/database/security"&gt;learn how to do it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are already familiar with Firebase, you might have noticed that this example doesn't use Firebase Authentication - Firebase's user management feature. That's because it way easier to do like that and at Mailmeteor we have some applications that run just this code. Mailmeteor is a bit different as we provide a Dashboard where users can sign in to send emails and manage their accounts. So we do use the Firebase Authentication layer.&lt;/p&gt;

&lt;p&gt;What's great about using Firebase, is that it lets you build lots of things easily. For example, Firebase lets you run a function as soon as a new field is created in your database. This can help you send an onboarding email sequence for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Integrating with Stripe
&lt;/h2&gt;

&lt;p&gt;So let's recap, from your add-on, using Apps Script and Firebase, you can now safely retrieve a user profile. All you need to do now is to update the user profile when someone pays you.&lt;/p&gt;

&lt;p&gt;We use Stripe because that's a relatively intuitive payment platform, but the following would work just as well with a PayPal account or any other service. We use Stripe Checkout at Mailmeteor to handle the checkout flow. If you want to manage the checkout flow yourself, you can do so &lt;a href="https://stripe.com/docs/payments/checkout"&gt;according to their docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What we will focus on here are &lt;a href="https://stripe.com/docs/webhooks/stripe-events"&gt;Stripe webhooks&lt;/a&gt;. In Stripe, you can create webhooks, so that Stripe can inform you whenever something happens. There are webhooks for just about anything (new customer, new subscription, new payment, new invoice, etc.).&lt;/p&gt;

&lt;p&gt;The webhook that you will use depends on the business model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;for lifetime subscriptions (i.e. the user pays you once and gets full access to the software for the rest of its life), you need to listen to the &lt;code&gt;checkout.session.completed&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;for recurring subscriptions (i.e. you are building a SaaS), you need to listen to multiple webhooks. At least: &lt;code&gt;webhook.checkout.session.completed&lt;/code&gt; and &lt;code&gt;customer.subscription.deleted&lt;/code&gt;. And depending on how advanced your SaaS is, you might need as well &lt;code&gt;customer.subscription.updated&lt;/code&gt; and &lt;code&gt;invoice.*&lt;/code&gt; events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To keep things simple, I'll show you how to listen to &lt;code&gt;checkout.session.completed&lt;/code&gt;. And if the business model of your add-on requires more, it's up to you to handle more webhooks events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Stripe webhook request signature&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Stripe webhook raw request&lt;/span&gt;
  &lt;span class="c1"&gt;// Note: "rawBody" is a Firebase method&lt;/span&gt;
  &lt;span class="c1"&gt;// @see: https://firebase.google.com/docs/functions/http-events&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify Stripe request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Handle the event&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;// Update profile in Firebase RTDB&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getUserIdFromEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FirebaseDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;webhook&lt;/code&gt; function updates the user's profile with the status &lt;code&gt;paid = true&lt;/code&gt; and the customer's ID (more on that later). This means that starting from now, whenever you retrieve the user's profile, you can show it all your paid features.&lt;/p&gt;

&lt;p&gt;Once you have deployed the &lt;code&gt;webhook&lt;/code&gt; function to Firebase, you can configure the webhook in Stripe Dashboard. Just retrieve the URL of your function in the Firebase console and paste it into Stripe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.stripe.com/webhooks"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FSNVRdYz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://mailmeteor.com/assets/img/blog/monetize-google-workspace-addon/stripe-webhooks.jpg" alt="Stripe Dashboard Webhooks Settings Page" width="880" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would highly recommend you use Stripe's test mode to make sure everything works great.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Customer portal
&lt;/h2&gt;

&lt;p&gt;When you start to monetize your add-on, your users will regularly ask you for their invoices. If you don't want to spend 50% of your support on these requests, it's best to create a customer portal where users can retrieve their invoices on their own.&lt;/p&gt;

&lt;p&gt;Good news: Stripe can help you with that! From your add-on, in the UI, you could add a new view where users can manage their accounts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zfe7G4mB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://mailmeteor.com/assets/img/blog/monetize-google-workspace-addon/addon-download-invoices.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zfe7G4mB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://mailmeteor.com/assets/img/blog/monetize-google-workspace-addon/addon-download-invoices.jpg" alt="Download your invoices in Mailmeteor add-on" width="715" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To add a link to the customer portal, you need to redirect your users to Cloud Functions which will create the Stripe URL to the customer's portal. The &lt;code&gt;portal&lt;/code&gt; function looks similar to this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;portal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_params&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_params&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getUserIdFromEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billingPortal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;customer&lt;/code&gt; value is saved at the webhook step, so you can reuse it easily. Also, for better security, we double-check that the customer's email in Stripe matches the provided user ID.&lt;/p&gt;

&lt;p&gt;Well, that was a lot?! You'll probably need a few things here and here to finalize the process. But I hope that's a good starting point to help you monetize your Google Workspace add-on.&lt;/p&gt;

&lt;p&gt;If you need more help to connect-the-dots, please feel free to &lt;a href="https://twitter.com/frenchcooc"&gt;reach out to me&lt;/a&gt;. And if you haven't an add-on yet, find some inspiration in our &lt;a href="https://mailmeteor.com/best-google-apps/"&gt;best Google add-ons directory&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>googleworkspace</category>
      <category>googlesheets</category>
      <category>gmail</category>
    </item>
    <item>
      <title>How to schedule tasks in more than 30 days in Google Cloud Tasks API?</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Mon, 28 Mar 2022 13:40:55 +0000</pubDate>
      <link>https://dev.to/mailmeteor/how-to-schedule-tasks-in-more-than-30-days-in-google-cloud-tasks-api-35f</link>
      <guid>https://dev.to/mailmeteor/how-to-schedule-tasks-in-more-than-30-days-in-google-cloud-tasks-api-35f</guid>
      <description>&lt;p&gt;At Mailmeteor, we rely heavily on &lt;a href="https://cloud.google.com/tasks"&gt;Google Cloud Tasks&lt;/a&gt; to send emails. In fact, each time we send an email, there's one (or more) Cloud Tasks associated to it. That's a lot of tasks in the end.&lt;/p&gt;

&lt;p&gt;While Google's product is really robust, one thing that has always being tricky is that you can't schedule a task that will run in more than 30 days.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maximum schedule time for a task&lt;/td&gt;
&lt;td&gt;30 days from current date and time&lt;/td&gt;
&lt;td&gt;The maximum amount of time in the future that a task can be scheduled.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Extract from &lt;a href="https://cloud.google.com/tasks/docs/quotas"&gt;Google Cloud Tasks documentation on quotas &amp;amp; limits&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is still way more than what AWS proposes (AWS SQS - Simple Queue Service - lets you queue messages for up to 15 minutes). Nevertheless, there are so many use cases when having a very-long tasks scheduler is needed.&lt;/p&gt;

&lt;p&gt;While I wasn't sure why Google has limited the execution delay to a month, one of their employee has explained on StackOverflow that such limit "&lt;em&gt;is a design decision. Google does not charge for the storage space of tasks, so extending that would detrimental to our costs.&lt;/em&gt;" (&lt;a href="https://stackoverflow.com/questions/58530361/how-increase-maximum-schedule-time-in-gcloud-tasks-api#comment103417942_58544657"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Though, Google Cloud Tasks is already a paid product. So extending the date, whether you need to pay for it or not, wouldn't be that much of an issue for them. In fact, according to &lt;a href="https://stackoverflow.com/questions/58530361/how-increase-maximum-schedule-time-in-gcloud-tasks-api#comment103417942_58544657"&gt;this StackOverflow thread&lt;/a&gt;, more than 1,000 people have been interested in extending the task delay. And there's already a &lt;a href="https://issuetracker.google.com/issues/175930182"&gt;feature request&lt;/a&gt;, back from 2020, which I urge you to star to make sure Google prioritizes this.&lt;/p&gt;

&lt;p&gt;Too much talking. Let's see how we can keep using Google Cloud Tasks and extend the execution delay "to infinity and beyond". &lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;The trick is in adding an &lt;code&gt;ETA&lt;/code&gt; header to your tasks. This way, before executing the task, you can check if the ETA is now (and thus execute the task) or in the future (and thus re-schedule the task). This way you can recursively keep creating tasks and eventually execute your task at your desired time.&lt;/p&gt;

&lt;p&gt;Let's take an example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a task to run in 45 days&lt;/li&gt;
&lt;li&gt;I create a new task with the max execution time (30 days)&lt;/li&gt;
&lt;li&gt;Then:

&lt;ul&gt;
&lt;li&gt;30 days later, the task executes, but it's too early, so I reschedule it in 45-30 = 14 days&lt;/li&gt;
&lt;li&gt;14 days later (45 days in total), the task executes normally.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In fact, doing so let's you create tasks in 1 year (or more) from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation (JS)
&lt;/h2&gt;

&lt;p&gt;In Express.js, all you need is a middleware that will check if the execution time is in the future, and if so will reschedule the tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Middleware to reschedule Google Cloud Tasks&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;googleTasksScheduleMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskETAHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google-cloud-tasks-eta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// If no header, skip middleware&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskETAHeader&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;taskETAHeader&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskETAHeader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Time has passed, let's process the task now&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intHeader&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// It's too early! Reschedule the task&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Construct the task.&lt;/span&gt;
    &lt;span class="nx"&gt;createTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Re-scheduled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, add your middle before the first routes of your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;googleTasksScheduleMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you can see, it's pretty easy to implement and doesn't require refactoring your application. If you are interested in more engineering articles from Mailmeteor, make sure to &lt;a href="https://dev.to/frenchcooc"&gt;follow my account&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>programming</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Cannot find name 'Bugsnag'</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Wed, 03 Nov 2021 13:28:07 +0000</pubDate>
      <link>https://dev.to/mailmeteor/cannot-find-name-bugsnag-1d8h</link>
      <guid>https://dev.to/mailmeteor/cannot-find-name-bugsnag-1d8h</guid>
      <description>&lt;p&gt;When you get started with Bugsnag, they have a handy guide to help you make sure errors are well reported to their monitoring tool. In &lt;a href="https://mailmeteor.com"&gt;Mailmeteor&lt;/a&gt;, we are using Bugsnag to make sure our tools are robust and bug-free.&lt;/p&gt;

&lt;p&gt;But recently, when initializing a new TypeScript project on Bugsnag, I end up having this error: &lt;code&gt;Cannot find name 'Bugsnag'&lt;/code&gt; (which might also be seen as &lt;code&gt;Bugsnag is not defined&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jUwszmly--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vyg9bgrli8wylabagfkc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jUwszmly--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vyg9bgrli8wylabagfkc.png" alt="Cannot find name 'Bugsnag'" width="540" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick fix
&lt;/h2&gt;

&lt;p&gt;Just add &lt;code&gt;import Bugsnag from '@bugsnag/js'&lt;/code&gt;. The type error is gone :-)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vue</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to add a sparkline to your Vue.js app</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Tue, 13 Jul 2021 14:02:31 +0000</pubDate>
      <link>https://dev.to/mailmeteor/how-to-add-a-sparkline-for-your-vue-js-app-1c4h</link>
      <guid>https://dev.to/mailmeteor/how-to-add-a-sparkline-for-your-vue-js-app-1c4h</guid>
      <description>&lt;p&gt;Very recently, I was looking to add a neat sparkline to a Vue.js application of my own. &lt;/p&gt;

&lt;p&gt;As always, I googled just that, looking for &lt;a href="https://www.google.com/search?q=sparkline+vue.js"&gt;sparkline vue.js&lt;/a&gt; or &lt;a href="https://www.google.com/search?q=sparkline+npm"&gt;sparkline npm&lt;/a&gt;. But I couldn't find something that was easy, with a small footprint and yet customizable.&lt;/p&gt;

&lt;p&gt;After playing a bit with &lt;a href="https://www.chartjs.org/"&gt;Chart.js&lt;/a&gt;, I just stopped and considered how I could build a decent, yet very simple, sparkline component (i.e. without any dependency).&lt;/p&gt;

&lt;p&gt;If you look at how npm's sparkline works as well as the ones from Stripe's dashboard, you will quickly realize that it's just a SVG element that you customize with JavaScript.&lt;/p&gt;

&lt;p&gt;So bear with me, I'll show you how to do just that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/sparkline-ex2dn"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Behind the scene
&lt;/h2&gt;

&lt;p&gt;The sparkline is just a Vue.js component where you provide the coordinates of the sparkline as an array. Here's how I've rendered the sparkline in the example above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;sparkline&lt;/span&gt; &lt;span class="na"&gt;v-bind:data=&lt;/span&gt;&lt;span class="s"&gt;"[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/sparkline&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The source code of the component is the following:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;As you might have noticed, the code renders an &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; HTML element by computing the coordinates of the different &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;There are two &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;. One for the blue line. And another one for the blue background. I've used the color &lt;code&gt;#1f8ceb&lt;/code&gt; but of course this is totally customizable, just like the width/height of the sparkline.&lt;/p&gt;

&lt;p&gt;That component is pretty basic and contrary to NPM or Stripe, it doesn't handle when a mouse hovers the &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;. I didn't need that for my use case, but if ever you implement it, feel free to edit the &lt;a href="https://gist.github.com/Frenchcooc/e4748ad6275984a01868153e3c0d8a1e"&gt;gist&lt;/a&gt; and share it with the community.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Basic mail merge script for Gmail (using Google Apps Script)</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Mon, 12 Apr 2021 15:04:32 +0000</pubDate>
      <link>https://dev.to/mailmeteor/basic-mail-merge-script-for-gmail-using-google-apps-script-4ok4</link>
      <guid>https://dev.to/mailmeteor/basic-mail-merge-script-for-gmail-using-google-apps-script-4ok4</guid>
      <description>&lt;p&gt;Sending mass emails from Gmail is sometimes seen as a challenge. I can tell you it's not! In this article we'll look at the basics of building a mail merge script for Gmail and how it can fit your email marketing needs.&lt;/p&gt;

&lt;p&gt;First, here's what you need to send emails in bulk with Gmail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Gmail (or Google Workspace) account&lt;/li&gt;
&lt;li&gt;a list of contacts&lt;/li&gt;
&lt;li&gt;a template of the emails you want to send&lt;/li&gt;
&lt;li&gt;basic development skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's see the 6 steps to reproduce to create your Google script to send emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorial: Building a mail merge script for Gmail
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://script.google.com/" rel="noopener noreferrer"&gt;Google Apps Script&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click on the "&lt;strong&gt;New project&lt;/strong&gt;" button to create a new project.&lt;/li&gt;
&lt;li&gt;You should now see the script editor.
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Fmail-merge-gmail-script-gas-editor.png" alt="Google Apps Script editor"&gt;
&lt;/li&gt;
&lt;li&gt;From the editor, copy and paste the following script:
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;

Update the &lt;code&gt;recipients&lt;/code&gt; variable with your list of recipients. Also update the &lt;code&gt;template&lt;/code&gt; subject and content to make it fit your needs&lt;/li&gt;
&lt;li&gt;Click on "&lt;strong&gt;Save&lt;/strong&gt;" to save your changes&lt;/li&gt;
&lt;li&gt;Then click on "&lt;strong&gt;▶️ Run&lt;/strong&gt;" to send your emails.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: the first time you use this script it will ask for the permission to send emails on your behalf. Then it won't ask you again.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Emails are usually immediately delivered, but sometimes it takes a few seconds. Check your &lt;a href="https://mail.google.com/mail/#sent" rel="noopener noreferrer"&gt;"Sent" folder in Gmail&lt;/a&gt; to confirm that all emails have been sent! As you will see, all your recipients receive a unique email sent from your email address. You don't need to use &lt;em&gt;bcc&lt;/em&gt; or any other techniques.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Some say that it's impossible to send emails in bulk with Gmail. It's totally untrue. Of course, this is a very basic macro for Google Sheets to send lots of emails with Gmail. You might need to adapt it a little bit more, but thanks to Google Apps Script you can do lot of things.&lt;/p&gt;

&lt;p&gt;Before you go, here are two things to keep in mind when using Gmail as an email marketing tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Be sure to abide by &lt;a href="https://support.google.com/mail/answer/81126?hl=en" rel="noopener noreferrer"&gt;Gmail bulk senders guidelines&lt;/a&gt;.&lt;/strong&gt; Especially, note that you are limited to send a reasonable amount of emails per day. If you have a &lt;code&gt;@gmail.com&lt;/code&gt; email address, you can send at most 500 emails/day while with a Google Workspace account, you sending limit hits 2000 emails per day. That's probably well enough for 99% of Gmail users, but it's good to have in mind those limits.&lt;/li&gt;
&lt;li&gt;If you are looking for more a advanced script, for example that let you send personalized emails (e.g. &lt;code&gt;Hi {{ firstname }}&lt;/code&gt;), I'd recommend you to use a Gmail mail merge tool such a &lt;a href="https://mailmeteor.com?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=mail-merge-gmail" rel="noopener noreferrer"&gt;Mailmeteor&lt;/a&gt; that does it for you. It already handles personalization, as well as dozens of features from attachments to aliases. Learn more about the &lt;a href="https://mailmeteor.com/features?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=mail-merge-gmail" rel="noopener noreferrer"&gt;features and benefits of Mailmeteor here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article is part of an extended guide on &lt;a href="https://mailmeteor.com/mail-merge-gmail/?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=mail-merge-gmail" rel="noopener noreferrer"&gt;Mail merge in Gmail (2021)&lt;/a&gt;. If you want to learn much more, go check it out!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to send a mail merge with Gmail?</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Wed, 03 Mar 2021 10:59:05 +0000</pubDate>
      <link>https://dev.to/mailmeteor/how-to-create-a-mail-merge-for-gmail-2e0c</link>
      <guid>https://dev.to/mailmeteor/how-to-create-a-mail-merge-for-gmail-2e0c</guid>
      <description>&lt;p&gt;There are 2 ways to mail merge in Gmail. You can either use a Google add-on that will do the job for you or build your own &lt;a href="https://mailmeteor.com/mail-merge-gmail/script" rel="noopener noreferrer"&gt;mail merge script in Gmail&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’ll cover both methods in  this guide. Even though we recommend using software built for that purpose which cover most issues and will probably save you time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: building a merge in Gmail using Google Apps Scripts
&lt;/h2&gt;

&lt;p&gt;As a developer, it's a good challenge to try to build your own Gmail mail merge without an add-on. And thankfully we’re going to use Google Apps Script, which makes it really easy to create Google add-ons.&lt;/p&gt;

&lt;p&gt;Google Developer Advocates have already released a great script to help us move forward with the code.&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://github.com/googleworkspace/solutions/blob/master/mail-merge/src/Code.js" rel="noopener noreferrer"&gt;the latest version of the open-source code hosted on GitHub&lt;/a&gt; written by Martin Hawksey - &lt;a class="mentioned-user" href="https://dev.to/mhawksey"&gt;@mhawksey&lt;/a&gt;. We gonna look at it step-by-step just after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Copyright Martin Hawksey 2020&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Licensed under the Apache License, Version 2.0 (the "License"); you may not&lt;/span&gt;
&lt;span class="c1"&gt;// use this file except in compliance with the License.  You may obtain a copy&lt;/span&gt;
&lt;span class="c1"&gt;// of the License at&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//     https://www.apache.org/licenses/LICENSE-2.0&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Unless required by applicable law or agreed to in writing, software&lt;/span&gt;
&lt;span class="c1"&gt;// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT&lt;/span&gt;
&lt;span class="c1"&gt;// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the&lt;/span&gt;
&lt;span class="c1"&gt;// License for the specific language governing permissions and limitations under&lt;/span&gt;
&lt;span class="c1"&gt;// the License.&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @OnlyCurrentDoc
*/&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Change these to match the column names you are using for email 
 * recipient addresses and email sent column.
*/&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RECIPIENT_COL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Recipient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMAIL_SENT_COL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email Sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/** 
 * Creates the menu item "Mail Merge" for user to run scripts on drop-down.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mail Merge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send Emails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sendEmails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Send emails from sheet data.
 * @param {string} subjectLine (optional) for the email draft message
 * @param {Sheet} sheet to read data from
*/&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subjectLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSheet&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// option to skip browser prompt if you want to use this code in other projects&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subjectLine&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="nx"&gt;subjectLine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mail Merge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                                      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Type or copy/paste the subject line of the Gmail &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                                      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft message you would like to mail merge with:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                      &lt;span class="nx"&gt;Browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Buttons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OK_CANCEL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subjectLine&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subjectLine&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; 
    &lt;span class="c1"&gt;// if no subject line finish up&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// get the draft Gmail message to use as a template&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getGmailTemplateFromDrafts_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subjectLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// get the data from the passed sheet&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDataRange&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Fetch displayed values for each row in the Range HT Andrew Roberts &lt;/span&gt;
  &lt;span class="c1"&gt;// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490&lt;/span&gt;
  &lt;span class="c1"&gt;// @see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDisplayValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// assuming row 1 contains our column headings&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 

  &lt;span class="c1"&gt;// get the index of column named 'Email Status' (Assume header names are unique)&lt;/span&gt;
  &lt;span class="c1"&gt;// @see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailSentColIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_SENT_COL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// convert 2d array into object array&lt;/span&gt;
  &lt;span class="c1"&gt;// @see https://stackoverflow.com/a/22917499/1027723&lt;/span&gt;
  &lt;span class="c1"&gt;// for pretty version see https://mashe.hawksey.info/?p=17869/#comment-184945&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{})));&lt;/span&gt;

  &lt;span class="c1"&gt;// used to record sent emails&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="c1"&gt;// loop through all the rows of data&lt;/span&gt;
  &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rowIdx&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="c1"&gt;// only send emails is email_sent cell is blank and not hidden by filter&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_SENT_COL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msgObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fillInTemplateFromObject_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// @see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)&lt;/span&gt;
        &lt;span class="c1"&gt;// if you need to send emails with unicode/emoji characters change GmailApp for MailApp&lt;/span&gt;
        &lt;span class="c1"&gt;// Uncomment advanced parameters as needed (see docs for limitations)&lt;/span&gt;
        &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;RECIPIENT_COL&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;msgObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msgObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;htmlBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msgObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;// bcc: 'a.bbc@email.com',&lt;/span&gt;
          &lt;span class="c1"&gt;// cc: 'a.cc@email.com',&lt;/span&gt;
          &lt;span class="c1"&gt;// from: 'an.alias@email.com',&lt;/span&gt;
          &lt;span class="c1"&gt;// name: 'name of the sender',&lt;/span&gt;
          &lt;span class="c1"&gt;// replyTo: 'a.reply@email.com',&lt;/span&gt;
          &lt;span class="c1"&gt;// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)&lt;/span&gt;
          &lt;span class="na"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;emailTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;inlineImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;emailTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inlineImages&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="c1"&gt;// modify cell to record email sent date&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// modify cell to record error&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_SENT_COL&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// updating the sheet with new data&lt;/span&gt;
  &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emailSentColIdx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Get a Gmail draft message by matching the subject line.
   * @param {string} subject_line to search for draft message
   * @return {object} containing the subject, plain and html message body and attachments
  */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGmailTemplateFromDrafts_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subject_line&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// get drafts&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drafts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDrafts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// filter the drafts that match subject line&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;drafts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;subjectFilter_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subject_line&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="c1"&gt;// get the message object&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// Handling inline images and attachments so they can be included in the merge&lt;/span&gt;
      &lt;span class="c1"&gt;// Based on https://stackoverflow.com/a/65813881/1027723&lt;/span&gt;
      &lt;span class="c1"&gt;// Get all attachments and inline image attachments&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allInlineImages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getAttachments&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;includeInlineImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;includeAttachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attachments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getAttachments&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;includeInlineImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;htmlBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBody&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 

      &lt;span class="c1"&gt;// Create an inline image object with the image name as key &lt;/span&gt;
      &lt;span class="c1"&gt;// (can't rely on image index as array based on insert order)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allInlineImages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;,{});&lt;/span&gt;

      &lt;span class="c1"&gt;//Regexp to search for all img string positions with cid&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imgexp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;img.*?src="cid:(.*?)".*?alt="(.*?)"[^&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;]+&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;htmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imgexp&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

      &lt;span class="c1"&gt;//Initiate the allInlineImages object&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inlineImagesObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="c1"&gt;// built an inlineImagesObj from inline image matches&lt;/span&gt;
      &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;inlineImagesObj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img_obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subject_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPlainBody&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;htmlBody&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; 
              &lt;span class="na"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inlineImages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inlineImagesObj&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Oops - can't find Gmail draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Filter draft objects with the matching subject linemessage by matching the subject line.
     * @param {string} subject_line to search for draft message
     * @return {object} GmailDraft object
    */&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subjectFilter_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subject_line&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getSubject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;subject_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Fill template string with data object
   * @see https://stackoverflow.com/a/378000/1027723
   * @param {string} template string containing {{}} markers which are replaced with data
   * @param {object} data object used to replace {{}} markers
   * @return {object} message replaced with data
  */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fillInTemplateFromObject_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// we have two templates one for plain text and the html body&lt;/span&gt;
    &lt;span class="c1"&gt;// stringifing the object means we can do a global replace&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;template_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// token replacement&lt;/span&gt;
    &lt;span class="nx"&gt;template_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;template_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/{{&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+}}/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;escapeData_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template_string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Escape cell data to make JSON safe
   * @see https://stackoverflow.com/a/9204218/1027723
   * @param {string} str to escape JSON special characters from
   * @return {string} escaped string
  */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;escapeData_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\"]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\\&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\/]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\b]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\f]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\n]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\r]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;r&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\t]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Follow the guidelines below to understand how to mail merge in Gmail using Apps Script:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Create a copy of the sample mail merge spreadsheet
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.google.com/spreadsheets/d/1EfjLuYGab8Xt8wCn4IokBIG0_W4tBtiU4vxl3Y7FPsA/copy" rel="noopener noreferrer"&gt;Open this demonstration spreadsheet&lt;/a&gt; and click on “Make a copy” to get your own copy.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. In your new spreadsheet
&lt;/h3&gt;

&lt;p&gt;Click on &lt;code&gt;Tools&lt;/code&gt; &amp;gt; &lt;code&gt;Script editor&lt;/code&gt; to open Google Apps Script. From there, you will see that a script is already tied to your spreadsheet. That's because you've made a copy of the previous spreadsheet!&lt;/p&gt;

&lt;p&gt;In the script editor, you can update the code as you wish. Changes will be reflected immediately.&lt;/p&gt;

&lt;p&gt;Back to copied spreadsheet, update the “Recipients” column with email addresses you would like to use in the mail merge. Replace the cells’ value under the “Recipients” column with your own email address for example.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Now, open Gmail to create a new draft email
&lt;/h3&gt;

&lt;p&gt;You can use personalized variables, like &lt;code&gt;{{ firstname }}&lt;/code&gt; which correspond to column names of the spreadsheet you just copied. This indicates text you’d like to be replaced with data from the copied spreadsheet.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Back in the copied spreadsheet, click on the custom menu item called “Mail Merge” and then click on “Send Emails”
&lt;/h3&gt;

&lt;p&gt;This item menu was created by the Apps Script project and will start the mail merge process.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. A dialog box appears for authorization. Read the authorization notice and continue
&lt;/h3&gt;

&lt;p&gt;Important note: The script we are using has been created and proofread by Google Apps Script teams. Always be super careful when authorizing scripts and third-party apps in general.&lt;/p&gt;

&lt;p&gt;When prompted enter or copy-paste the subject line used in your draft Gmail message. Then click OK.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Sending your emails
&lt;/h3&gt;

&lt;p&gt;You will see that the “Email Sent” column will update with the message status. Back in Gmail, check your Sent folder and review the emails the program just sent for you!&lt;/p&gt;



&lt;p&gt;Keep in mind that using a script is at your own risk. Check Gmail’s sending limit before sending large volumes of emails. Especailly, be aware that your account can get blocked by Gmail if your emailing activity seems unusual in the eyes of anti-spam filters.&lt;/p&gt;

&lt;p&gt;For these reasons, we would recommend using a mail merge solution such as Mailmeteor. Mailmeteor deals with all these aspects for you and ensures that your privacy remains protected.&lt;/p&gt;
&lt;h2&gt;
  
  
  Method 2: using a mail merge add-on like Mailmeteor
&lt;/h2&gt;

&lt;p&gt;We’ll start with a real-life example to show you how to do a mail merge from Gmail using a Google Sheets add-on. In this example, we’re using &lt;a href="https://mailmeteor.com?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=mail-merge-gmail" rel="noopener noreferrer"&gt;Mailmeteor&lt;/a&gt;, the best rated Google mail merge add-on.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Get Mailmeteor
&lt;/h3&gt;

&lt;p&gt;All you have to do is to &lt;a href="https://gsuite.google.com/marketplace/app/mailmeteor_mail_merge_for_gmail/1008170693301" rel="noopener noreferrer"&gt;install Mailmeteor from the Google Workspace Marketplace&lt;/a&gt;. The Worskpace Marketplace is a place where you can find all the apps compatible with your Google Suite. Mailmeteor is a tool that integrates with Gmail and Google Sheets to merge emails with Gmail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-install.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-install.png" alt="Install Mailmeteor for Google Sheets"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Add contact in Google Sheets
&lt;/h3&gt;

&lt;p&gt;Once Mailmeteor is installed, &lt;a href="http://sheets.new/" rel="noopener noreferrer"&gt;open a Google Sheets spreadsheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First of all, you will need to add recipients to a Google Sheets spreadsheet. This spreadsheet will be the place where you store your contact list. You will also be able to track your campaign metrics from there.&lt;/p&gt;

&lt;p&gt;To create a mailing list you can either add your recipients manually or import contacts. To import contacts in Google Sheets, go to &lt;em&gt;Menu &amp;gt; File &amp;gt; Import&lt;/em&gt; and select your Excel or .csv file.&lt;/p&gt;

&lt;p&gt;Here’s a mail merge demo spreadsheet we’re going to use:&lt;/p&gt;


  &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-spreadsheet-demo-mail-merge.png" alt="Demo spreadsheet to mail merge in Gmail"&gt;&lt;a href="https://docs.google.com/spreadsheets/d/1RAJPfeW8ehVYGpu6nziKuWmFbog8hJSlcTyKT7FO7iY/edit?usp=drive_web&amp;amp;ouid=110634960934586502748" rel="nofollow noreferrer noopener"&gt;Link to mail merge demo spreadsheet&lt;/a&gt;
  


&lt;p&gt;Note: when opening Mailmeteor for the first time, you will be guided through a quick onboarding tutorial. A demo spreadsheet like this one will be created for you.&lt;/p&gt;

&lt;p&gt;Let’s breakdown how your spreadsheet should look like:&lt;/p&gt;

&lt;p&gt;➤ &lt;strong&gt;Add column headers on the 1st row&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mailmeteor will pull the information from your spreadsheet to personalize your emails. Each column represents a personalized field. This field will be replaced in your email template.&lt;/p&gt;

&lt;p&gt;In our example, we have 4 columns named: firstname, email, company, postscriptum.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-spreadsheet-demo-mail-merge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-spreadsheet-demo-mail-merge.png" alt="Demo spreadsheet to mail merge in Gmail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add as many columns as you want and pick any column header name you want. Make sure you have a column named “email”.&lt;/p&gt;

&lt;p&gt;➤ &lt;strong&gt;Fill the columns with your recipients’ information&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fill your spreadsheet with your recipients’ info. Ensure all email cells are filled with valid email addresses. Apart from the emails, you can leave some cells blank, that is fine! In the example below, some recipients will get a Post Scriptum whereas others won't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-spreadsheet-demo-add-columns.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-spreadsheet-demo-add-columns.gif" alt="Fill spreadsheet cells to mail merge in Mailmeteor"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Open Mailmeteor from the Add-ons menu in Google Sheets
&lt;/h3&gt;

&lt;p&gt;Once your contact list is ready, open Mailmeteor. To open Mailmeteor go to the menu and select Add-ons&amp;gt; Mailmeteor &amp;gt; Open Mailmeteor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-addon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-addon.png" alt="Mailmeteor add-on to mail merge in Google Sheets"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Mailmeteor interface. It tells you how many emails you can send per day and details related to your campaign. Next, we are going to compose the template that will be used for the mail merge.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Compose a new email template
&lt;/h3&gt;

&lt;p&gt;Click on the “Create new template” button. This will open an editor in which you can compose your email. The Mailmeteor editor is the exact same as Gmail, you will find all the actions you need to customize your email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-compose-template.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-compose-template.gif" alt="Create a new template in Mailmeteor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we’re going to personalize your email. Personalizing emails is important as it helps make your recipients feel unique when they receive your emails. Using personalization will also dramatically improve your opening rates - and thus the replies you will get.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Personalize your emails
&lt;/h3&gt;

&lt;p&gt;A mail merge transforms a standard email template into a personalized email copy. It’s done by replacing variables fields within the template with the content from your spreadsheet.&lt;/p&gt;

&lt;p&gt;To insert a variable it’s easy: add variables using double brackets like this &lt;code&gt;{{ firstname }}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here is a template you can copy-paste:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;When adding a variable, always make sure that it matches a header in your spreadsheet.&lt;/p&gt;

&lt;p&gt;Once you are satisfied with your template, click the “&lt;strong&gt;Save&lt;/strong&gt;” button.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Preview emails before sending
&lt;/h3&gt;

&lt;p&gt;Mailmeteor offers a preview feature that is super helpful to review emails before sending. The preview mode gives you a glimpse of the actual output of your email once personalized for each recipient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-preview-mail-merge.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-preview-mail-merge.gif" alt="Preview mail merge before sending"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also send a test email to yourself. Testing your emails on several devices is a best practice. This will ensure your emails will display correctly in most situations.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Send your mail merge
&lt;/h3&gt;

&lt;p&gt;Ready for take-off? It’s time to send your mailmerge campaign.&lt;/p&gt;

&lt;p&gt;We know that sending your mail merge can be a bit daunting at first. No worries though, if you follow these steps, everything will be alright!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-sending-mail-merge.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmailmeteor.com%2Fassets%2Fimg%2Fblog%2Fmail-merge-gmail%2Ftutorial-mailmeteor-sending-mail-merge.gif" alt="Sending a mail merge with Mailmeteor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✨ That's it! You are now ready to mail merge emails with Gmail using an add-on such as Mailmeteor ✨&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here's a real-life example. Watch this teacher use Mailmeteor to mail merge emails to his students:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/hfy5UKw36nA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Now it’s your turn
&lt;/h2&gt;

&lt;p&gt;That’s it for this guide to mail merge in Gmail. We hope you now better understand this simple yet incredibly powerful tool called mail merge.&lt;/p&gt;

&lt;p&gt;This guide is part of an extended guide on &lt;a href="https://mailmeteor.com/mail-merge-gmail/?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=mail-merge-gmail" rel="noopener noreferrer"&gt;Mail merge in Gmail (2021)&lt;/a&gt;. If you want to learn much more, go check it out!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>mailmerge</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From 0 to integrations, in less than 5 minutes</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Thu, 29 Oct 2020 10:47:39 +0000</pubDate>
      <link>https://dev.to/frenchcooc/from-0-to-integrations-in-less-than-5-minutes-1hnl</link>
      <guid>https://dev.to/frenchcooc/from-0-to-integrations-in-less-than-5-minutes-1hnl</guid>
      <description>&lt;p&gt;APIs that use OAuth provides the best experience for their users. But from a developer's perspective, well... it's a real nightmare. &lt;/p&gt;

&lt;p&gt;It doesn't have to be like that. That's why a few months ago, we open-sourced Pizzly, an API Integrations Manager, dedicated to solve all OAuth's pains for developers.  &lt;/p&gt;

&lt;p&gt;It provides everything a developer needs to go from 0 to integrations, in less than 5 minutes. 80+ pre-configured APIs, OAuth-dance, refresh token, integrations dashboard, JavaScript SDKs, &lt;a href="https://dev.to/bearer/introducing-pizzly-an-open-sourced-free-fast-simple-api-integrations-manager-4jog"&gt;and more&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, &lt;a href="https://www.producthunt.com/posts/pizzly"&gt;Pizzly is on Product Hunt 🤩&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/GIRa5uIWOD8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It's the best time to show us some love to an open-source project. I'm looking forward to see you there 🚀&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>api</category>
    </item>
    <item>
      <title>Introducing Pizzly - an open-sourced, free, fast &amp; simple API Integrations Manager</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Mon, 14 Sep 2020 08:07:30 +0000</pubDate>
      <link>https://dev.to/bearer/introducing-pizzly-an-open-sourced-free-fast-simple-api-integrations-manager-4jog</link>
      <guid>https://dev.to/bearer/introducing-pizzly-an-open-sourced-free-fast-simple-api-integrations-manager-4jog</guid>
      <description>&lt;p&gt;Within my company, Bearer, the whole team is focused on helping developers that rely on third-party APIs. In 2019, our engineers developed a solution that eased how to integrate with any API that uses OAuth.&lt;/p&gt;

&lt;p&gt;It saved hours of engineering time when working with API integrations, by handling both the authentication strategy (with refresh tokens) as well as proxying the request.&lt;/p&gt;

&lt;p&gt;As we believe no developers should ever have to spend hours dealing with the ins and outs of integrating OAuth API, we’ve decided to fully open-source our tool. Introducing &lt;a href="https://github.com/bearer/pizzly" rel="noopener noreferrer"&gt;Pizzly: The OAuth Integration Proxy&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/NangoHQ" rel="noopener noreferrer"&gt;
        NangoHQ
      &lt;/a&gt; / &lt;a href="https://github.com/NangoHQ/nango" rel="noopener noreferrer"&gt;
        nango
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A single API for all your integrations.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;What is Pizzly?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Bearer/Pizzly" rel="noopener noreferrer"&gt;Pizzly&lt;/a&gt; is an OAuth Integrations Manager. It provides everything a developer needs to easily consume an OAuth-based API (&lt;em&gt;aka an API that uses OAuth as the authentication method&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Most APIs now use the &lt;a href="https://oauth.net/" rel="noopener noreferrer"&gt;OAuth framework&lt;/a&gt; to authorize an application that wants to access a user's data. One of the main reasons is that OAuth provides the best user experience while being very secure. But one thing has been totally forgotten in the OAuth framework: the developer experience.&lt;/p&gt;

&lt;p&gt;It is much more difficult for a developer to use an OAuth based API than it is to use an API that relies on an API Key. Try timing yourself on how long it takes to integrate with two APIs. Let's see how long it takes to have your first successful request to the Stripe API compared to the Google Sheets API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fttfac-without-pizzly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fttfac-without-pizzly.png" alt="Time To First API Call (TTFAC) without Pizzly"&gt;&lt;/a&gt;&lt;/p&gt;
Time To First API Call (TTFAC) without Pizzly



&lt;p&gt;It took me around 43 minutes and 19 seconds to perform an authenticated request to Google Sheets starting from scratch (&lt;a href="https://gist.github.com/Frenchcooc/aa39ca7001975d23b8f5db4b046fc1cc" rel="noopener noreferrer"&gt;source&lt;/a&gt;). And only 5 minutes for Stripe (&lt;a href="https://gist.github.com/Frenchcooc/32dfc8a2dbbd5a3acb54e8e705d958a7" rel="noopener noreferrer"&gt;source&lt;/a&gt;). &lt;strong&gt;We should change that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pizzly aims to provide the best developer experience when using an OAuth-based API. One of its power-features is that it completely handles the OAuth-dance (including refreshing a token), meaning a developer can focus on requesting endpoints without spending hours dealing with authentication.&lt;/p&gt;

&lt;p&gt;Here's the same test using Pizzly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fttfac-with-pizzly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fttfac-with-pizzly.png" alt="TTFAC with Pizzly"&gt;&lt;/a&gt;&lt;/p&gt;
Time To First API Call (TTFAC) with Pizzly



&lt;p&gt;It took me almost the same amount of time (5 min vs 7 min) to perform an authenticated request to the Google Sheets API compared with Stripe, starting from scratch with Pizzly. Again, here's the &lt;a href="https://gist.github.com/Frenchcooc/4c75ac43d0cb0da3a550ffccf85cf4e5" rel="noopener noreferrer"&gt;source&lt;/a&gt;. And contrary to the previous test, using Pizzly means the token will be refreshed once expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How does it work?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Pizzly provides multiple tools to help developers with their API integrations, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  an auth service - &lt;em&gt;to handle the OAuth-dance&lt;/em&gt;;&lt;/li&gt;
&lt;li&gt;  a proxy - &lt;em&gt;to make authenticated requests to an API&lt;/em&gt;;&lt;/li&gt;
&lt;li&gt;  a dashboard - &lt;em&gt;to enable and configure APIs&lt;/em&gt;;&lt;/li&gt;
&lt;li&gt;  a JS library - &lt;em&gt;to connect a user from your frontend.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two major services are Auth and Proxy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The auth service handles the OAuth-dance and generates what is called an &lt;code&gt;authId&lt;/code&gt; each time a user has successfully authorized your OAuth application. The &lt;code&gt;authId&lt;/code&gt; acts as a reference to the OAuth payload (i.e. &lt;code&gt;access token&lt;/code&gt; and &lt;code&gt;refresh token&lt;/code&gt;). While the access token and refresh token expire and change over time, the authId is always the same. Think of it as something like a user identity.&lt;/li&gt;
&lt;li&gt; The proxy service forwards HTTP requests to the third-party APIs. To authenticate the request, the developer sends the &lt;code&gt;authId&lt;/code&gt; alongside. This tells the proxy service to transform the request and authenticate it with the right access token. In case it has expired, Pizzly will refresh the token and retry the request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Another great tool that Pizzly brings is the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fpizzly-dashboard.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bearer.sh%2Fcontent%2Fimages%2F2020%2F07%2Fpizzly-dashboard.jpg" alt="Pizzly dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dashboard lets you configure your integrations, test them, and look at what's happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to use it?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Pizzly is self-hosted, meaning you can install it on your machine or any platform-as-a-service (e.g. Heroku, AWS, etc.). Here's a guide on &lt;a href="https://github.com/Bearer/Pizzly#getting-started" rel="noopener noreferrer"&gt;how to get started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once installed, enable an API from the list and trigger a complete OAuth-dance in a few lines of code using the Pizzly JS library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize Pizzly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pizzly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pizzly&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pizzly.example.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pizzly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Connect a user to an API&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;authId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sucessfully connected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;It failed!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user is successfully connected, you retrieve an &lt;code&gt;authId&lt;/code&gt; that you can use to make authenticated requests to the API. Here's a cURL on how to make a request to the Google Sheets API to create a spreadsheet on the user's drive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST /proxy/google-sheets/ &lt;span class="se"&gt;\\&lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pizzly-Auth-Id: REPLACE-WITH-YOUR-AUTH-ID"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No matter what your backend is running on (Ruby, Go, Node.js), send your requests to the proxy. It makes integrating APIs much faster. You can even perform your calls from your frontend, with the &lt;a href="https://github.com/Bearer/Pizzly/tree/master/src/clients/javascript" rel="noopener noreferrer"&gt;JS client&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Some demo&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some great project are already using Pizzly to handle the API requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/picsoung/airtable-shipping-block" rel="noopener noreferrer"&gt;Airtable Shipping Block&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/bearer/how-to-integrate-with-the-google-sheets-api-in-2-minutes-569o"&gt;Push to Google Sheets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub fetch profile (tutorial) in &lt;a href="https://dev.to/frenchcooc/how-to-make-api-calls-in-react-g1e"&gt;React&lt;/a&gt; or &lt;a href="https://dev.to/bearer/how-to-use-an-oauth-based-api-in-vue-js-1elo"&gt;Vue.js&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Psst.&lt;/em&gt; At &lt;a href="https://www.bearer.sh" rel="noopener noreferrer"&gt;Bearer.sh&lt;/a&gt;, Pizzly is a full part of our API integrations flow, to manage Slack notifications for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How it can help you?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FBearer%2FPizzly%2Fmaster%2Fviews%2Fassets%2Fimg%2Fdocs%2Fpizzly-preconfigured-apis.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FBearer%2FPizzly%2Fmaster%2Fviews%2Fassets%2Fimg%2Fdocs%2Fpizzly-preconfigured-apis.jpg" alt="https://raw.githubusercontent.com/Bearer/Pizzly/master/views/assets/img/docs/pizzly-preconfigured-apis.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pizzly supports more than 50 APIs out-of-the-box. All you need to do is set your credentials and scopes in the dashboard. This list includes the most common APIs, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Communication APIs&lt;/strong&gt;: Gmail, Microsoft Teams, Slack, Zoom;&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;CRM&lt;/strong&gt;: Front, Hubspot, Salesforce, etc.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Developer tools&lt;/strong&gt;: BitBucket, GitHub, GitLab, etc.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Finance APIs&lt;/strong&gt;: Xero, Sellsy, Zoho Books, etc.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Productivity&lt;/strong&gt;: Asana, Google Drive, Google Sheets, Jira, Trello, etc.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Social APIs&lt;/strong&gt;: Facebook, LinkedIn, Reddit, etc.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/Bearer/Pizzly/wiki/Supported-APIs" rel="noopener noreferrer"&gt;and more...&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that's not all. Each pre-configured API is a &lt;code&gt;.json&lt;/code&gt; file located &lt;a href="https://github.com/Bearer/Pizzly/tree/master/integrations" rel="noopener noreferrer"&gt;within the &lt;code&gt;/integrations/&lt;/code&gt; folder&lt;/a&gt;. So if you want an API that is not pre-configured yet, you can set up the configuration as a new &lt;code&gt;.json&lt;/code&gt; file within that directory. If ever you feel like sharing, create a new PR to share your own configuration with the community.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Built with&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js ⚡️&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.passportjs.org/" rel="noopener noreferrer"&gt;Passport.js&lt;/a&gt; - To handle the OAuth dance 🕺&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://knexjs.org/" rel="noopener noreferrer"&gt;Knex&lt;/a&gt; - To manage requests to the database 📃&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ejs.co/" rel="noopener noreferrer"&gt;EJS&lt;/a&gt; - For the dashboard templating ⚙️&lt;/li&gt;
&lt;li&gt;Vanilla JavaScript - For some magic 🍦&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Support us&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Bearer/Pizzly" rel="noopener noreferrer"&gt;Star the repo on GitHub&lt;/a&gt;, Tweet, share among your friends, teams and contacts!&lt;/p&gt;

&lt;p&gt;You can also get in touch directly with me via email at &lt;a href="mailto:corentin@bearer.sh"&gt;corentin@bearer.sh&lt;/a&gt; or on Twitter &lt;a class="mentioned-user" href="https://dev.to/frenchcooc"&gt;@frenchcooc&lt;/a&gt;. And if you really want to help us make Pizzly better, &lt;a href="https://github.com/Bearer/Pizzly" rel="noopener noreferrer"&gt;contribute to the GitHub repo&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>news</category>
      <category>webdev</category>
      <category>node</category>
      <category>showdev</category>
    </item>
    <item>
      <title>⚡️ How to call an OAuth based API in React?</title>
      <dc:creator>Corentin</dc:creator>
      <pubDate>Wed, 02 Sep 2020 15:43:19 +0000</pubDate>
      <link>https://dev.to/mailmeteor/how-to-make-api-calls-in-react-g1e</link>
      <guid>https://dev.to/mailmeteor/how-to-make-api-calls-in-react-g1e</guid>
      <description>&lt;p&gt;Do you know what Facebook, Google, GitHub, and thousands more APIs have in common? They use OAuth to authenticate requests.&lt;/p&gt;

&lt;p&gt;OAuth, especially OAuth 2.0, is now everywhere. It's a very powerful authentication framework that powers up developers to have granularity over the data that it needs.&lt;/p&gt;

&lt;h1&gt;
  
  
  React + OAuth = 🤔
&lt;/h1&gt;

&lt;p&gt;When it comes to OAuth-based API, your React app is not well-suited to send requests. You just can't hide your API keys deep into your codebase. Someone will easily find it.&lt;/p&gt;

&lt;p&gt;What you need to do is to set up some backend service, that proxies requests to the third-party API. It can be fun to do, but it's a long process for a quick API call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For example, you will need a backend to protect your API keys. Declare routes, understand packages (&lt;em&gt;like Passport.js&lt;/em&gt;), proxying requests, and dozens of more actions. It doesn't have to be so hard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today, I'll showcase an open-source project that I'm actively contributing to. It's called &lt;a href="https://github.com/Bearer/Pizzly"&gt;Pizzly&lt;/a&gt; and it helps a lot with using API from a single page application.&lt;/p&gt;

&lt;p&gt;Let's see how it looks like with a simple demo:&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/rq78z?view=preview"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Curious about how you can do it on your application? Here's a full guide.&lt;/p&gt;

&lt;h1&gt;
  
  
  The React skeleton 🦴
&lt;/h1&gt;

&lt;p&gt;To learn how to make API calls to an API, we first need a React skeleton. And the least that we need is an app that consumes an API endpoint using OAuth2.&lt;/p&gt;

&lt;p&gt;As you probably have a GitHub account, we will use that API, but you can easily switch to any other API that uses OAuth2 (Slack, Salesforce, ...) or OAuth1 (Twitter, Trello, ...).&lt;/p&gt;

&lt;p&gt;Here's how the application will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Pizzly&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pizzly-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setProfile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"App"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Click the button bellow to retrieve your GitHub profile using&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"noopener noreferrer"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://github.com/Bearer/Pizzly"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Pizzly
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        .
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Retrieve your GitHub profile&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a very basic React application that renders the user's profile as a plain JSON when it has been fetched. Otherwise, it asks the user to connect to GitHub.&lt;/p&gt;

&lt;h1&gt;
  
  
  The authentication 🔐
&lt;/h1&gt;

&lt;p&gt;Here, we gonna use &lt;a href="https://github.com/Bearer/Pizzly"&gt;Pizzly&lt;/a&gt;, the open-source project I told you about a few lines above. &lt;/p&gt;

&lt;p&gt;It provides a &lt;code&gt;.connect()&lt;/code&gt; method that triggers an authentication flow from your frontend, which you can handle with callbacks. No need to create a redirect URL, deal with backend, etc.&lt;/p&gt;

&lt;p&gt;Let's see how to update the skeleton above to use with Pizzly.&lt;/p&gt;

&lt;p&gt;First, we need to initialize Pizzly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize Pizzly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pizzly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Pizzly&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_HOSTNAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publishableKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_PUBLISHABLE_KEY&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pizzly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;setupId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_SETUP_ID_GITHUB_DEMO_APP&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we add a new &lt;code&gt;connect()&lt;/code&gt; method to trigger the authentication flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * The connect method lets us authenticate a user
   * to our GitHub application (i.e. the OAuth dance)
   */&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;github&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;authId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sucessfully connected!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;fetchProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A few lines of code in your application and you are ready to handle any OAuth based API 💪.&lt;/p&gt;

&lt;h1&gt;
  
  
  The configuration 🤖
&lt;/h1&gt;

&lt;p&gt;Pizzly is a self-hosted OAuth manager. This means that you need to host it somewhere, for example on Heroku (it takes 30 seconds). Once hosted, you can access Pizzly's dashboard, which is where you configure your GitHub integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wr_62uUl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/Bearer/Pizzly/master/views/assets/img/docs/pizzly-dashboard-all-apis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wr_62uUl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/Bearer/Pizzly/master/views/assets/img/docs/pizzly-dashboard-all-apis.png" alt="Pizzly dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To deploy your own Pizzly instance right now, click on any of the following button:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Heroku&lt;/th&gt;
&lt;th&gt;Platform.sh&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://heroku.com/deploy?template=https://github.com/Bearer/Pizzly" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--26lElwvG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://console.platform.sh/projects/create-project/?template=https://github.com/Bearer/Pizzly&amp;amp;utm_campaign=deploy_on_platform?utm_medium=button&amp;amp;utm_source=affiliate_links&amp;amp;utm_content=https://github.com/Bearer/Pizzly" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D7yKEFCs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://platform.sh/images/deploy/deploy-button-lg-blue.svg" alt="Deploy with Platform.sh"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then, select the GitHub API. And configure it by saving your application's client ID, client credentials and scopes. You will get them from GitHub by following &lt;a href="https://docs.github.com/en/developers/apps/creating-an-oauth-app"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once your Pizzly instance is created and you have configured a GitHub application, edit your React application with the following values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pizzly environment variables, make sure to replace&lt;/span&gt;
&lt;span class="c1"&gt;// these with those of your own Pizzly instance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_HOSTNAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_PUBLISHABLE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PIZZLY_SETUP_ID_GITHUB_DEMO_APP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The least that you need is &lt;code&gt;PIZZLY_HOSTNAME&lt;/code&gt;. The two others are optional.&lt;/p&gt;

&lt;h1&gt;
  
  
  An authenticated API request 🎉
&lt;/h1&gt;

&lt;p&gt;Alright, we have already spend a few minutes on the configuration. Let's move back to funny things.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.github.com/en/rest/reference/users#get-the-authenticated-user"&gt;GitHub API provides a handy endpoint (&lt;code&gt;/user&lt;/code&gt;)&lt;/a&gt; to retrieve the profile of the authenticated user. This endpoint uses OAuth authentication, so it looks like a good use case.&lt;/p&gt;

&lt;p&gt;Let's add a new method to our application to do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * The fetchProfile method retrieves the authenticated
   * user profile on GitHub (the request is proxied through Pizzly)
   */&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;em&gt;voilà&lt;/em&gt;!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;NB: To authenticate the request, we have provided an &lt;code&gt;authId&lt;/code&gt;. It's like a user identity for that particular API. It was generated (and saved) with the &lt;code&gt;connect()&lt;/code&gt; method.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  What's next? 💡
&lt;/h1&gt;

&lt;p&gt;You now know how to authenticate a user towards any OAuth based API using React. If you prefer Vue.js, &lt;a href="https://dev.to/bearer/how-to-use-an-oauth-based-api-in-vue-js-1elo"&gt;the same tutorial is available for Vue.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's easily adaptable to &lt;a href="https://github.com/Bearer/Pizzly/wiki/Supported-APIs"&gt;all the most famous APIs&lt;/a&gt;. No need to create backend routes or understand every single concept of OAuth. Pizzly takes care of that for you (and for the experts, Pizzly automatically refreshes the &lt;code&gt;access_token&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Again, &lt;a href="https://codesandbox.io/s/pizzly-github-react-demo-rq78z"&gt;have a look at the CodeSandbox&lt;/a&gt; to have a full understanding of the code and share your thoughts/questions in the comments below.&lt;/p&gt;

&lt;p&gt;And if you've liked this tutorial, please add a star to Pizzly on GitHub. Here's the link: &lt;a href="https://github.com/Bearer/Pizzly"&gt;https://github.com/Bearer/Pizzly&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>pizzly</category>
    </item>
  </channel>
</rss>
