<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by theTribe on Medium]]></title>
        <description><![CDATA[Stories by theTribe on Medium]]></description>
        <link>https://medium.com/@thetribe.io?source=rss-9d4a9a6a01d------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*B6AiWx-11kJSJJlLT5rhcg.png</url>
            <title>Stories by theTribe on Medium</title>
            <link>https://medium.com/@thetribe.io?source=rss-9d4a9a6a01d------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 09:24:33 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@thetribe.io/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Managing spreadsheets with Symfony]]></title>
            <link>https://medium.com/thetribeio/managing-spreadsheets-with-symfony-4e6a1808eca7?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/4e6a1808eca7</guid>
            <category><![CDATA[symfony]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Fri, 05 Oct 2018 13:56:23 GMT</pubDate>
            <atom:updated>2018-10-05T13:56:23.783Z</atom:updated>
            <content:encoded><![CDATA[<p>Despite digital development, spreadsheets are still very popular to store and display data as text and numerals or in graphical form. Modern spreadsheets also provide built-in functions for common financial and statistical operations. Thus managing spreadsheets in a web application can be really helpful to allow users to keep their habits.</p><p><a href="https://phpspreadsheet.readthedocs.io/en/develop/">PhpSpreadsheet</a> is a library written in pure PHP providing a set of classes that allow to read from and to write to different spreadsheet file formats, like Excel and LibreOffice Calc. Let’s see how to use it with a Symfony app.</p><h3>Prerequisites</h3><p>First, we need PHP version 5.6 or newer and ensure that PHP extensions php_zip, php_xml and php_gd2 are enabled before developing using PhpSpreadsheet.</p><p>Then use composer to install <a href="https://packagist.org/packages/phpoffice/phpspreadsheet">PhpSpreadsheet</a> into our project:</p><pre>composer require phpoffice/phpspreadsheet 1.0.0</pre><h3>How to create a basic spreadsheet?</h3><p>In this first part, we will see how to create a spreadsheet showing characteristics of some browsers (name, developer, release date and programming language used).</p><h3>Spreadsheet creation</h3><p>First we must create a workbook using the Spreadsheet class. Once we have a single worksheet, it is time to write some data. The main advantage of PhpSpreadsheet is that all parts of our code is easy to understand even by a non-developer as it describes simple actions that are usually done by a user writing in spreadsheet applications. Thus, we will construct our spreadsheet accessing cells and settings values.</p><pre>use PhpOffice\PhpSpreadsheet\Spreadsheet;<br><br>protected function createSpreadsheet()<br>{<br>    $spreadsheet = new Spreadsheet();<br>    // Get active sheet - it is also possible to retrieve a specific sheet<br>    $sheet = $spreadsheet-&gt;getActiveSheet();<br><br>    // Set cell name and merge cells<br>    $sheet-&gt;setCellValue(&#39;A1&#39;, &#39;Browser characteristics&#39;)-&gt;mergeCells(&#39;A1:D1&#39;);<br><br>    // Set column names<br>    $columnNames = [<br>        &#39;Browser&#39;,<br>        &#39;Developper&#39;,<br>        &#39;Release date&#39;,<br>        &#39;Written in&#39;,<br>    ];<br>    $columnLetter = &#39;A&#39;;<br>    foreach ($columnNames as $columnName) {<br>        // Allow to access AA column if needed and more<br>        $columnLetter++;<br>        $sheet-&gt;setCellValue($columnLetter.&#39;2&#39;, $columnName);<br>    }<br><br>    // Add data for each column<br>    $columnValues = [<br>        [&#39;Google Chrome&#39;, &#39;Google Inc.&#39;, &#39;September 2, 2008&#39;, &#39;C++&#39;],<br>        [&#39;Firefox&#39;, &#39;Mozilla Foundation&#39;, &#39;September 23, 2002&#39;, &#39;C++, JavaScript, C, HTML, Rust&#39;],<br>        [&#39;Microsoft Edge&#39;, &#39;Microsoft&#39;, &#39;July 29, 2015&#39;, &#39;C++&#39;],<br>        [&#39;Safari&#39;, &#39;Apple&#39;, &#39;January 7, 2003&#39;, &#39;C++, Objective-C&#39;],<br>        [&#39;Opera&#39;, &#39;Opera Software&#39;, &#39;1994&#39;, &#39;C++&#39;],<br>        [&#39;Maxthon&#39;, &#39;Maxthon International Ltd&#39;, &#39;July 23, 2007&#39;, &#39;C++&#39;],<br>        [&#39;Flock&#39;, &#39;Flock Inc.&#39;, &#39;2005&#39;, &#39;C++, XML, XBL, JavaScript&#39;],<br>    ];<br><br>    $i = 3; // Beginning row for active sheet<br>    foreach ($columnValues as $columnValue) {<br>        $columnLetter = &#39;A&#39;;<br>        foreach ($columnValue as $value) {<br>            $columnLetter++<br>            $sheet-&gt;setCellValue($columnLetter.$i, $value);<br>        }<br>        $i++;<br>    }<br><br>    return $spreadsheet;<br>}</pre><h3>Formatting</h3><p>PhpSpreadsheet also provides a way to set style to our spreadsheet thanks to several classes gathered in the \PhpSpreadsheet\Style folder. The following code shows basic methods that can be used to center text, change font weight and autosize column.</p><pre>use PhpOffice\PhpSpreadsheet\Style\Alignment;<br><br>$columnLetter = &#39;A&#39;;<br>foreach ($columnNames as $columnName) {<br>    // Center text<br>    $sheet-&gt;getStyle($columnLetter.&#39;1&#39;)-&gt;getAlignment()-&gt;setHorizontal(Alignment::HORIZONTAL_CENTER);<br>    $sheet-&gt;getStyle($columnLetter.&#39;2&#39;)-&gt;getAlignment()-&gt;setHorizontal(Alignment::HORIZONTAL_CENTER);<br>    // Text in bold<br>    $sheet-&gt;getStyle($columnLetter.&#39;1&#39;)-&gt;getFont()-&gt;setBold(true);<br>    $sheet-&gt;getStyle($columnLetter.&#39;2&#39;)-&gt;getFont()-&gt;setBold(true);<br>    // Autosize column<br>    $sheet-&gt;getColumnDimension($columnLetter)-&gt;setAutoSize(true);<br>    $columnLetter++;<br>}</pre><h3>Managing file formats and save spreadsheet</h3><p>Now, we have our spreadsheet correctly set but we still want to use it in spreadsheet applications. Supported file formats are listed <a href="https://phpspreadsheet.readthedocs.io/en/develop/#file-formats-supported">here</a>. We will then have to choose a format to write our spreadsheet into a file.</p><p>To easily choose between famous spreadsheet formats, we will create a simple form like this:</p><pre>use Symfony\Component\Form\AbstractType;<br>use Symfony\Component\Form\Extension\Core\Type\ChoiceType;<br>use Symfony\Component\Form\FormBuilderInterface;<br><br>public function buildForm(FormBuilderInterface $builder, array $options)<br>{<br>    $builder<br>        -&gt;add(&#39;format&#39;, ChoiceType::class, [<br>            &#39;choices&#39; =&gt; [<br>                &#39;xlsx&#39; =&gt; &#39;xlsx&#39;,<br>                &#39;ods&#39; =&gt; &#39;ods&#39;,<br>                &#39;csv&#39; =&gt; &#39;csv&#39;,<br>            ],<br>            &#39;label&#39; =&gt; false,<br>            &#39;placeholder&#39; =&gt; &#39;Select a format&#39;,<br>        ])<br>    ;<br>}</pre><p>We can use this form in our controller to create correct file according to a given format and then stream the response back to the client.</p><pre>use AppBundle\Form\Type\ExcelFormatType;<br>use PhpOffice\PhpSpreadsheet\Writer\Csv;<br>use PhpOffice\PhpSpreadsheet\Writer\Ods;<br>use PhpOffice\PhpSpreadsheet\Writer\Xlsx;<br>use Symfony\Component\HttpFoundation\StreamedResponse;<br><br>/**<br> * @Route(&quot;/export&quot;, name=&quot;export&quot;)<br> */<br>public function exportAction(Request $request)<br>{<br>    $form = $this-&gt;createForm(ExcelFormatType::class);<br>    $form-&gt;handleRequest($request);<br>    if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {<br>        $data = $form-&gt;getData();<br>        $format = $data[&#39;format&#39;];<br>        $filename = &#39;Browser_characteristics.&#39;.$format;<br><br>        $spreadsheet = $this-&gt;createSpreadsheet();<br><br>        switch ($format) {<br>            case &#39;ods&#39;:<br>                $contentType = &#39;application/vnd.oasis.opendocument.spreadsheet&#39;;<br>                $writer = new Ods($spreadsheet);<br>                break;<br>            case &#39;xlsx&#39;:<br>                $contentType = &#39;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#39;;<br>                $writer = new Xlsx($spreadsheet);<br>                break;<br>            case &#39;csv&#39;:<br>                $contentType = &#39;text/csv&#39;;<br>                $writer = new Csv($spreadsheet);<br>                break;<br>            default:<br>                return $this-&gt;render(&#39;AppBundle::export.html.twig&#39;, [<br>                    &#39;form&#39; =&gt; $form-&gt;createView(),<br>                ]);<br>        }<br><br>        $response = new StreamedResponse();<br>        $response-&gt;headers-&gt;set(&#39;Content-Type&#39;, $contentType);<br>        $response-&gt;headers-&gt;set(&#39;Content-Disposition&#39;, &#39;attachment;filename=&quot;&#39;.$filename.&#39;&quot;&#39;);<br>        $response-&gt;setPrivate();<br>        $response-&gt;headers-&gt;addCacheControlDirective(&#39;no-cache&#39;, true);<br>        $response-&gt;headers-&gt;addCacheControlDirective(&#39;must-revalidate&#39;, true);<br>        $response-&gt;setCallback(function() use ($writer) {<br>            $writer-&gt;save(&#39;php://output&#39;);<br>        });<br><br>        return $response;<br>    }<br><br>    return $this-&gt;render(&#39;AppBundle::export.html.twig&#39;, [<br>        &#39;form&#39; =&gt; $form-&gt;createView(),<br>    ]);<br>}</pre><h3>How to retrieve data from a spreadsheet?</h3><p>Once we are able to create spreadsheets, we also need to know how to read from existing ones. In this second part, we will see how to retrieve data from the spreadsheet we have just created.</p><h3>Loader or Reader</h3><p>To load a workbook, it is possible to simply use static load() method of the \PhpOffice\PhpSpreadsheet\IOFactory class. This method will attempt to identify the file type and instantiate the corresponding loader in order to load the file.</p><pre>use PhpOffice\PhpSpreadsheet\IOFactory;<br><br>protected function loadFile($filename)<br>{<br>    return IOFactory::load($filename);<br>}</pre><p>If we do not want to be able to manage all supported file types or if we want to add some specific logic, we can instantiate a specific reader object for our file and use the reader’s load() method to correctly read the file.</p><p>Moreover, if we are not sure of the file type, it is possible to use the IO Factory’s identifiy() method combined with createReader() method to identify and instantiate the reader object we need.</p><pre>use PhpOffice\PhpSpreadsheet\Reader\Csv as ReaderCsv;<br>use PhpOffice\PhpSpreadsheet\Reader\Ods as ReaderOds;<br>use PhpOffice\PhpSpreadsheet\Reader\Xlsx as ReaderXlsx;<br><br>protected function readFile($filename)<br>{<br>    $extension = pathinfo($filename, PATHINFO_EXTENSION);<br>    switch ($extension) {<br>        case &#39;ods&#39;:<br>            $reader = new ReaderOds();<br>            break;<br>        case &#39;xlsx&#39;:<br>            $reader = new ReaderXlsx();<br>            break;<br>        case &#39;csv&#39;:<br>            $reader = new ReaderCsv();<br>            break;<br>        default:<br>            throw new \Exception(&#39;Invalid extension&#39;);<br>    }<br>    $reader-&gt;setReadDataOnly(true);<br>    return $reader-&gt;load($filename);<br>}</pre><h3>Retrieve data</h3><p>Now that we have a spreadsheet, we can iterate through each worksheet it contains to construct an array containing browser characteristics.</p><pre>protected function createDataFromSpreadsheet($spreadsheet)<br>{<br>    $data = [];<br>    foreach ($spreadsheet-&gt;getWorksheetIterator() as $worksheet) {<br>        $worksheetTitle = $worksheet-&gt;getTitle();<br>        $data[$worksheetTitle] = [<br>            &#39;columnNames&#39; =&gt; [],<br>            &#39;columnValues&#39; =&gt; [],<br>        ];<br>        foreach ($worksheet-&gt;getRowIterator() as $row) {<br>            $rowIndex = $row-&gt;getRowIndex();<br>            if ($rowIndex &gt; 2) {<br>                $data[$worksheetTitle][&#39;columnValues&#39;][$rowIndex] = [];<br>            }<br>            $cellIterator = $row-&gt;getCellIterator();<br>            $cellIterator-&gt;setIterateOnlyExistingCells(false); // Loop over all cells, even if it is not set<br>            foreach ($cellIterator as $cell) {<br>                if ($rowIndex === 2) {<br>                    $data[$worksheetTitle][&#39;columnNames&#39;][] = $cell-&gt;getCalculatedValue();<br>                }<br>                if ($rowIndex &gt; 2) {<br>                    $data[$worksheetTitle][&#39;columnValues&#39;][$rowIndex][] = $cell-&gt;getCalculatedValue();<br>                }<br>            }<br>        }<br>    }<br><br>    return $data;<br>}</pre><h3>Displaying data</h3><p>Let’s assume that our spreadsheets are all located in a folder named export and that we want to retrieve data from a specific spreadsheet named Browser_characteristics.xlsx.</p><p>By combining the two previous parts, we can display our data with a basic Twig template.</p><pre>/**<br> * @Route(&quot;/import&quot;, name=&quot;import&quot;)<br> */<br>public function importAction(Request $request)<br>{<br>    $filename = $this-&gt;get(&#39;kernel&#39;)-&gt;getRootDir().&#39;/../export/Browser_characteristics.xlsx&#39;;<br>    if (!file_exists($filename)) {<br>        throw new \Exception(&#39;File does not exist&#39;);<br>    }<br><br>    $spreadsheet = $this-&gt;readFile($filename);<br>    $data = $this-&gt;createDataFromSpreadsheet($spreadsheet);<br><br>    return $this-&gt;render(&#39;AppBundle::import.html.twig&#39;, [<br>        &#39;data&#39; =&gt; $data,<br>    ]);<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/615/0*999c9fsyfcMBeOy_.png" /><figcaption>Import result</figcaption></figure><h3>Conclusion</h3><p>With this article, you are now able to correctly manage PhpSpreadsheet basic usages. If you need an example of this test case, we got a <a href="https://github.com/thetribeio/phpspreadsheet-test">github repository</a> to help you test that!</p><p>If you have other questions related to PhpSpreadsheet, don’t hesitate to comment down below, and we’ll try our best to answer quickly and if you are interested in other articles about PhpSpreadsheet be sure to tell us!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4e6a1808eca7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/managing-spreadsheets-with-symfony-4e6a1808eca7">Managing spreadsheets with Symfony</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making a recurring payment with Stripe, manage subscriptions and more]]></title>
            <link>https://medium.com/thetribeio/making-a-recurring-payment-with-stripe-manage-subscriptions-and-more-30539c8e2833?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/30539c8e2833</guid>
            <category><![CDATA[stripe]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Fri, 05 Oct 2018 13:24:16 GMT</pubDate>
            <atom:updated>2018-10-05T13:24:16.269Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://stripe.com/">Stripe</a> is a powerful payment service designed for developers, but it can be difficult to use for newcomers even if <a href="https://stripe.com/docs">the documentation</a> is really good. For a simple integration (credit card payment) see our previous post <a href="http://aroundthefire.io/stripe-simple-payment">Making a simple payment with Stripe</a> (it’s with a PHP backend but you’ll get the idea).</p><p>Here we will go over the following subjects:</p><ul><li>Subscriptions/recurring payments</li><li>Lifecycle of subscriptions</li><li>Coupons</li><li>SEPA payments</li></ul><h3>Prerequisites</h3><p>Prerequisites are the same as for the previous post. You need a <a href="https://dashboard.stripe.com/register">configured stripe account</a> and once it’s done you will need <a href="https://dashboard.stripe.com/account/apikeys">your api keys</a>.</p><h3>Subscriptions</h3><h4>Subscription workflow</h4><p>First thing to know: a subscription is a recurring payment handled by Stripe. This means that Stripe needs to know about your customer and a valid payment method. To detail the process, here is what you should do:</p><ul><li>Create a Stripe Customer</li><li>Create a Stripe Source (payment method)</li><li>Create a Stripe Subscription</li></ul><h4>Customer creation</h4><p>If for some reason you already created a Stripe Customer and/or Source it is a best practice to reuse it by fetching it using the cus_id/src_id provided by Stripe at the creation time.</p><p>A bit of node.js code for a getOrCreateStripeCustomer function using the stripe npm package:</p><pre>import stripePackage from &#39;stripe&#39;;</pre><pre>   const stripe = stripePackage(process.env.STRIPE_SECRET_KEY);</pre><pre>    const getOrCreateStripeCustomer = async (user) =&gt; {<br>    let customer;<br>    const customerId = user.stripeCustomerId;<br>    if (customerId) {<br>        customer = await stripe.customers.retrieve(customerId);<br>    } else {<br>        customer = await stripe.customers.create({<br>            email: user.email,<br>        });<br>        await user.update({ stripeCustomerId: customer.id });<br>    }</pre><pre>    return customer;<br>};</pre><p>In this piece of code, user represent the user in my application and it has a stripeCustomerId attribute. It is also worth noting that my STRIPE_SECRET_KEY is pushed into the environment but you can manage this part however you like. It is important to mention that the key is <strong>secret</strong> and should not appear in any commit or public file.</p><h4>Source creation</h4><p>Now that we have a method to create a customer we must attach a source to this customer. The source creation usually happens in the frontend with Stripe. It’s a best practice to keep it that way so that no sensible information goes through your servers, it reduces the risk of having your clients banking data stolen and let Stripe handle all the risky data. In our case we were in a ReactJS environment so we used the <a href="https://www.npmjs.com/package/react-stripe-elements">react-stripe-elements package</a>. We also included the <a href="https://www.npmjs.com/package/iban">iban package</a> to check that provided IBAN numbers for SEPA payments are valid.</p><p>The payment UI looks like this:</p><pre>import IBAN from &#39;iban&#39;;<br>import { flowRight } from &#39;lodash/fp&#39;;<br>import PropTypes from &#39;prop-types&#39;;<br>import React from &#39;react&#39;;<br>import { reduxForm, formValues } from &#39;redux-form&#39;;<br>import { CardElement, injectStripe } from &#39;react-stripe-elements&#39;;</pre><pre>const PaymentForm = ({<br>    stripe, paymentMedium, iban, owner,<br>}) =&gt; {<br>    const stripeSourceCreation = async (event) =&gt; {<br>        event.preventDefault();<br>        let source;<br>        if (&#39;card&#39; === paymentMedium) {<br>            // Stripe Elements magic gets the only card elements available and create the source<br>            source = await stripe.createSource();<br>        } else {<br>            source = await stripe.createSource({<br>                type: &#39;sepa_debit&#39;,<br>                sepa_debit: { iban },<br>                currency: &#39;eur&#39;,<br>                owner,<br>            });<br>        }<br>        if (source) {<br>            const postBody = {<br>                stripeSourceId: source.source.id,<br>                paymentMedium,<br>            };<br>            // Here you POST the above body to link source to customer in the backend (see below)<br>        }<br>    };</pre><pre>const checkIBAN = (event) =&gt; {<br>        const { target } = event;<br>        target.classList.remove(&#39;valid&#39;);<br>        target.classList.remove(&#39;invalid&#39;);<br>        if (IBAN.isValid(iban)) {<br>            target.classList.add(&#39;valid&#39;);<br>        } else {<br>            target.classList.add(&#39;invalid&#39;);<br>        }<br>    };</pre><pre>return (<br>        &lt;form onSubmit={stripeSourceCreation}&gt;<br>            &lt;p&gt;Payment by { &#39;card&#39; === paymentMedium ? &#39;card&#39; : &#39;SEPA&#39; }&lt;/p&gt;<br>            { &#39;card&#39; === paymentMedium ? (<br>                &lt;CardElement /&gt;<br>            ) : (<br>                &lt;Fragment&gt;<br>                    &lt;span&gt;IBAN Number:&lt;/span&gt;<br>                    &lt;input<br>                        placeholder=&quot;ex: FR68539007547034&quot;<br>                        onBlur={checkIBAN}<br>                        name=&quot;iban&quot;<br>                    /&gt;<br>                    &lt;span&gt;Account owner:&lt;/span&gt;<br>                    &lt;input placeholder=&quot;ex: Marie Dupont&quot; name=&quot;owner&quot; /&gt;<br>                    &lt;p&gt;<br>                        Here you should include the SEPA Mandate provided by Stripe<br>                    &lt;/p&gt;<br>                &lt;/Fragment&gt;<br>            )}<br>        &lt;/form&gt;<br>    );<br>};</pre><pre>PaymentForm.propTypes = {<br>    stripe: PropTypes.shape().isRequired,<br>    paymentMedium: PropTypes.string.isRequired,<br>    iban: PropTypes.string,<br>    owner: PropTypes.string,<br>};</pre><pre>export default flowRight([<br>    reduxForm({<br>        form: &#39;create-stripe-source-form&#39;,<br>        type: &#39;simulation-form&#39;,<br>    }),<br>    formValues({<br>        iban: &#39;iban&#39;,<br>    }),<br>    injectStripe,<br>])(PaymentForm);</pre><p>Once created in the frontend you should get the sourceId returned by stripe and link it to your client (this you probably should do in your backend). It’s a simple Stripe API call:</p><pre>stripe.customers.createSource(stripeCustomerId, { source: stripeSourceId });</pre><p>Finally you can create the subscription. The first step for this happens in the Stripe dashboard where you must create your “plans”. Go to the <a href="https://dashboard.stripe.com/subscriptions/products">Billing &gt; Products</a> page and create you product and then inside the product, create your pricing plan. When creating the plan you set an ID which you will use in your subscription code:</p><pre>const subscription = await stripe.subscriptions.create({<br>    customer: stripeCustomerId,<br>    items: [{ plan: stripeSubscriptionPlanId }],<br>});<br>// then update you user to save the fact that the plan is activated!</pre><p>And now your subscription is in place, it will keep running until further notice. Every month (or whatever billing interval you choose) the customer will be charged with the subscription amount on the payment source you registered. Unless there is a payment failure… This is where webhooks arrive on scene.</p><h3>Webhooks and subscriptions lifecycle</h3><p>Webhooks allow Stripe to proactively warn you about events occurring in Stripe operations. It can be a payment failure, a revoked source, a deleted customer… All of these events you can be notified about (there is more than seventy different events, we will only talk about one as an example but they all work the same way).</p><p>First you must go in your Stripe dashboard, in the “Developers” section and “Webhooks” subsection. Click on the “+ Add endpoint” button and set your endpoint URL. You can decide to send all events to the endpoint or just a subset. Finally you must save the “Signing secret” to check for legitimate requests on your endpoint. Once this is done you have to create your endpoint. I let you choose your preferred way to handle it, just be careful to match the URL you put in the Stripe interface and to listen for the POST method.</p><p>In your endpoint you will need to do the following actions:</p><ul><li>Identify the legitimacy of the request</li><li>Extract Stripe data</li><li>Do things in reaction to the event</li></ul><p>The first two points are easy as pie. Given you are using express, here is the sample to extract data and validate request:</p><pre>import stripePackage from &#39;stripe&#39;;</pre><pre>const { body, header } = req; // req is your express request object<br>const stripe = stripePackage(process.env.STRIPE_SECRET_KEY);<br>const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;<br>const stripeEvent = stripe.webhooks.constructEvent(body, headers[&#39;stripe-signature&#39;], webhookSecret);</pre><p>And here you are, with the data sent by stripe webhooks!</p><p>Last step consists in doing things depending on the event type and/or content and this is totally up to you and the specificities of your processes. I just give you the <a href="https://stripe.com/docs/api#event_types">link to the documentation describing the events</a>.</p><p>While coding your webhooks you should keep in mind that a best pratice is to send a status 200 response to Stripe ensuring them you got the message before doing any heavy lifting.</p><h3>Coupons management</h3><p>Once your subscriptions are in place, you may want to add some coupon system to handle marketing operations, special events, or angry users.</p><p>First thing to know, Stripe coupons can be used for subscriptions <em>only</em>. No coupons for one time payments. If you want to do that, you are doomed to make the system in-house. Stripe sends you to a <a href="https://stripe.com/docs/recipes/coupons-for-charges">recipe</a> to do it in rails but anyway you have to engineer that piece. It means manage a full CRUD for coupons and applying them on payment before interacting with Stripe.</p><p>If you are going for a standard subscription only coupons, you can manage those directly in the Stripe dashboard in the <a href="https://dashboard.stripe.com/coupons">Billing &gt; Coupons</a> menu. Same as for subscriptions, you will set an ID for the coupon (which is the one users should enter when paying to get reductions). This ID entered by users you only have to push it to Stripe alongside the subscriptions like this:</p><pre>await stripe.subscriptions.create({<br>    customer: stripeCustomerId,<br>    items: [{ plan: stripeSubscriptionPlanId }],<br>    coupon: couponID,<br>});</pre><p>This way the coupon will be applied on the subscription. You can also update an existing subscription to apply a coupon on it.</p><p>Last detail, if you need to create coupons on the fly, it’s possible using the <a href="https://stripe.com/docs/api#coupons">Coupon API</a>. An example is available in the <a href="https://stripe.com/docs/billing/subscriptions/discounts#creating-coupons">Stripe doc about discounts</a>.</p><p>Finally, for Europeans it’s common to use SEPA payments. Let’s dig into that!</p><h3>Easy setup for SEPA… or not</h3><p>SEPA (Single Euro Payments Area) is a mean to pay in some places in Europe. It has some drawbacks (which you will learn below) and also a processing workflow which is a bit more complicated than cards. The main reason being that SEPA needs to be accepted on the client side, and it can take up to 14 days. This asynchronous process adds some complexity to the whole thing.</p><h3>Activate SEPA</h3><p>To setup a SEPA payment method, all you have to do is prepare to lot of administrative process and a tiny little bit of code. SEPA payment method (named sepa_debit in the Stripe API) is not activated by default. You must go to Stripe website into the “payments → settings” panel and ask for “SEPA Direct Debit” activation. And… wait. SEPA debit can officially be activated only after 30 to 60 days of payments activity on cards. You can try and contact the Stripe team for an early activation and they will warn you about the usage of SEPA which has some drawbacks:</p><ul><li>Users can cancel the payment through SEPA within 8 weeks after the withdrawal without providing any reason. This is considered pure loss by Stripe and you have no way to prevent or defend against this.</li><li>Users can dispute the payment 11 months after the 8 first weeks (this means your cash is secured only 13 months after payment). These disputes are decided at the bank levels, Stripe may ask more information from you. If the bank decides to cancel the payment, your stripe balance will be reduced accordingly and another 7.5 € will be withdrawed (the dispute process fee).</li><li>Any failed payment through SEPA will lead to a 7.5 € fee you will have to pay (for example if your client does not have enough cash on their account)</li></ul><p>If after all that you still want to go on, be warned that the Stripe test API will stay closed to all your SEPA related calls until you are activated.</p><p>As for the setup, if you went this far, you already have the code to create the source in the UI code in the “Source Creation” section. And the subscription doesn’t really care for the source type, it just charge any source you gave. The real work is in the webhooks since SEPA is prone to more payment failures, but this too is already in your hand. I have nothing more to say, to your keyboards!</p><h3>Conclusion</h3><p>So that’s pretty much all you need to know to setup your subscriptions management with Stripe.</p><p>If you have other questions related to Stripe, don’t hesitate to comment down below, and we’ll try our best to answer quickly and if you are interested in other articles about Stripe be sure to tell us!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=30539c8e2833" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/making-a-recurring-payment-with-stripe-manage-subscriptions-and-more-30539c8e2833">Making a recurring payment with Stripe, manage subscriptions and more</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making a simple payment with Stripe (API Version 2017–08–15)]]></title>
            <link>https://medium.com/thetribeio/making-a-simple-payment-with-stripe-api-version-2017-08-15-3910bafe5838?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/3910bafe5838</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[stripe]]></category>
            <category><![CDATA[payments]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Wed, 03 Oct 2018 15:07:07 GMT</pubDate>
            <atom:updated>2018-10-03T15:07:07.467Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://stripe.com/">Stripe</a> is a powerful payment service designed for developers, but it can be difficult to use for newcomers even if <a href="https://stripe.com/docs">the documentation</a> is really good. Let’s make an example for a simple payment with a credit card on a Symfony app.</p><h3>Prerequisites</h3><p>Be sure to <a href="https://dashboard.stripe.com/register">create an account</a> and configure it, you won’t be able to test anything without a Stripe account as you will need <a href="https://dashboard.stripe.com/account/apikeys">your api keys</a> for that.</p><p>There are several ways to work with Stripe and achieve a single payment with a credit card, in this article we will tokenize the card details and then charge this card.</p><h3>Front</h3><h3>Set up the Stripe.js script</h3><p>First things first, you need to import the <a href="https://stripe.com/docs/stripe.js?">Stripe.js library</a> and instantiate the stripe object, which can easily be done with the following code:</p><pre>&lt;script type=&quot;text/javascript&quot; src=&quot;<a href="https://js.stripe.com/v3/">https://js.stripe.com/v3/</a>&quot;&gt;&lt;/script&gt;<br>&lt;script&gt;<br>    const stripe = Stripe(&#39;pk_test_UseYourPublishableKeyHere&#39;);<br>&lt;/script&gt;</pre><p>Stripe recommends you not only to import the script on the payment pages of your site, but on every page, as it packages an <a href="https://stripe.com/docs/radar">advanced fraud functionality</a>.</p><h3>Create our payment form</h3><p>Now that we have prepared the script, we need a form to let the user put his card details. We will use the <a href="https://stripe.com/docs/elements">Elements</a> to create our form, so we can use the following:</p><pre>&lt;form action=&quot;/charge&quot; method=&quot;post&quot; id=&quot;payment-form&quot;&gt;<br>    &lt;div class=&quot;form-row&quot;&gt;<br>        &lt;label for=&quot;card-element&quot;&gt;<br>            Credit or debit card<br>        &lt;/label&gt;<br>        &lt;div id=&quot;card-element&quot;&gt;<br>            &lt;!-- a Stripe Element will be inserted here. --&gt;<br>        &lt;/div&gt;</pre><pre>&lt;!-- Used to display Element errors --&gt;<br>        &lt;div id=&quot;card-errors&quot; role=&quot;alert&quot;&gt;&lt;/div&gt;<br>    &lt;/div&gt;</pre><pre>&lt;button&gt;Submit Payment&lt;/button&gt;<br>&lt;/form&gt;</pre><p>For now if you try to display the page, all you have is an empty form without any input. We need to instantiate the Element in our script to fill the form with a Stripe form for the card:</p><pre>const elements = stripe.elements();<br>const card = elements.create(&#39;card&#39;);<br>card.mount(&#39;#card-element&#39;);</pre><p>We will now retrieve a set of inputs allowing the user to fill its card details. Those inputs come with a nice UX and error management so you can concentrate on more important things.</p><h3>Create the token</h3><p>Everything is ready, so now let’s get on with the source creation. The first thing to do with our form is to create a token with the payment details. The token creation ensures that you do not pass any sensitive data to your server. Thanks to the card Element, this is easy, all we need to do is to call the method stripe.createToken(card). Add some error management and use the promise sent back to obtain the following:</p><pre>const form = document.getElementById(&#39;payment-form&#39;);<br>form.addEventListener(&#39;submit&#39;, function(event) {<br>    event.preventDefault();</pre><pre>    stripe.createToken(card).then(function(result) {<br>        if (result.error) {<br>            // Inform the user if there was an error<br>            const errorElement = document.getElementById(&#39;card-errors&#39;);<br>            errorElement.textContent = result.error.message;<br>        } else {<br>            // Send the token to your server<br>            stripeTokenHandler(result.token);<br>        }<br>    });<br>});</pre><p>Now that we have our token, we need to send it to our server, and we can do it thanks to a hidden input:</p><pre>function stripeTokenHandler(token) {<br>    // Insert the token ID into the form so it gets submitted to the server<br>    const form = document.getElementById(&#39;payment-form&#39;);<br>    const hiddenInput = document.createElement(&#39;input&#39;);<br>    hiddenInput.setAttribute(&#39;type&#39;, &#39;hidden&#39;);<br>    hiddenInput.setAttribute(&#39;name&#39;, &#39;stripeToken&#39;);<br>    hiddenInput.setAttribute(&#39;value&#39;, token.id);<br>    form.appendChild(hiddenInput);</pre><pre>    // Submit the form<br>    form.submit();<br>}</pre><p>Please note that the Element’s fields are not sent to the server with this code, meaning that you are operating in a <a href="https://stripe.com/docs/security">PCI compliant</a> way. This way, we can send all the data we had in our form with the token and your server never receives sensitive card details from your request (card number, expiracy date, cvc).</p><h3>Backend</h3><p>For the backend we will be using <a href="https://symfony.com/">Symfony</a> and <a href="https://getcomposer.org/">Composer</a> to install dependencies. In order to have something reusable later, we will create a service for all our Stripe methods.</p><h3>Install</h3><p>To use Stripe in our backend Application, we need to install the <a href="https://github.com/stripe/stripe-php">stripe-php library</a>. All you need to do is to run the following command:</p><pre>composer require stripe/stripe-php</pre><h3>Stripe Service</h3><p>For starters, we need to save our API keys. Let’s add our key in the app/config/parameters.yml file, like so:</p><pre>stripe_secret_key: sk_test_UseYourSecretKeyHere stripe_publishable_key: pk_test_UseYourPublishableKeyHere</pre><p>This way, we can specify for each instance the keys to use, test keys for the test instance and the live keys for our production environment.</p><p>Now we can create our service. This service will need the secret key to initialize the Stripe library, so it will look like this:</p><pre>&lt;?php<br>namespace AppBundle\Services;</pre><pre>use Stripe\Stripe;</pre><pre>class StripeService<br>{<br>    public function __construct(string $stripeSecretKey)<br>    {<br>        Stripe::setApiKey($stripeSecretKey);<br>    }<br>}</pre><p>And now we can register our service in the app/config/services.yml config file:</p><pre>AppBundle\Services\StripeService:<br>    arguments: [&#39;%stripe_secret_key%&#39;]<br>    public: true</pre><p>Now that we have a functional service for the Stripe library, let’s make our first payment.</p><h3>Payment Request</h3><p>Now let’s implement the charge mechanism in our Stripe service:</p><pre>/**<br> * <a href="http://twitter.com/throws">@throws</a> \Stripe\Error\Card<br> */<br>public function charge(string $token)<br>{<br>    $charge = \Stripe\Charge::create([<br>        &#39;amount&#39; =&gt; 4200,   // Amount in cents<br>        &#39;currency&#39; =&gt; &#39;eur&#39;,<br>        &#39;description&#39; =&gt; &#39;Example charge&#39;,<br>        &#39;source&#39; =&gt; $token,<br>    ]);</pre><pre>    return $charge;<br>}</pre><p>With this simple code, we can process a payment of 42.00€ (passed in cents to the API), with the card corresponding to the token given. The description is a text that will appear in your Stripe dashboard and on the client bank receipt. Now all we have to do is use this method in our controller :</p><pre>/**<br> * <a href="http://twitter.com/Route">@Route</a>(&quot;/charge&quot;, name=&quot;charge_simple&quot;)<br> * <a href="http://twitter.com/Method">@Method</a>({&quot;POST&quot;})<br> */<br>public function chargeAction(Request $request)<br>{<br>    if (!$request-&gt;request-&gt;has(&#39;stripeToken&#39;)) {<br>        return new BadRequestHttpException(&#39;No stripe token to create charge&#39;);<br>    }</pre><pre>    $charge = $this-&gt;container<br>        -&gt;get(StripeService::class)<br>        -&gt;charge($request-&gt;request-&gt;get(&#39;stripeToken&#39;));</pre><pre>    $amount = $charge-&gt;amount / 100; // Amount is in cents</pre><pre>    $this-&gt;addFlash(<br>        &#39;notice&#39;,<br>        &quot;Charge of $amount € successful !&quot;<br>    );</pre><pre>    return $this-&gt;redirectToRoute(&#39;homepage&#39;);<br>}</pre><p>This action is pretty straightforward, we retrieve the token, charge the card, and display a message. Clean and simple.</p><p>But what if there is a problem and the card cannot be charged? The charge method of the Stripe library throws an exception that will help you manage those errors, so you can do something like this:</p><pre>try {<br>    $charge = $this-&gt;get(StripeService::class)-&gt;charge($params[&#39;stripeToken&#39;]);</pre><pre>    $amount = $charge-&gt;amount / 100; // Amount is in cents<br>    $this-&gt;addFlash(<br>        &#39;notice&#39;,<br>        &quot;Charge of $amount € successful !&quot;<br>    );<br>} catch (\Stripe\Error\Card $e) {<br>    $this-&gt;addFlash(<br>        &#39;error&#39;,<br>        $e-&gt;getMessage()<br>    );<br>}</pre><p>The message of the exception is readable by your customers (if they are english speakers). If you want to provide internationalization, you can rely on the Stripe error code, that you can get with $e-&gt;getStripeCode().</p><h3>Conclusion</h3><p>So that’s pretty much all you need to know to process payments with Stripe. The front part is almost done by Stripe itself and the back is easy to use, and everything is secure, so it’s worth a try. If you need an example of this test case, we got a <a href="https://github.com/thetribeio/stripe-test">github repository</a> to help you test that!</p><p>If you have other questions related to Stripe, don’t hesitate to comment down below, and we’ll try our best to answer quickly and if you are interested in other articles about Stripe be sure to tell us!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3910bafe5838" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/making-a-simple-payment-with-stripe-api-version-2017-08-15-3910bafe5838">Making a simple payment with Stripe (API Version 2017–08–15)</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[La satisfaction client chez theTribe]]></title>
            <link>https://medium.com/thetribeio/la-satisfaction-client-chez-thetribe-cf811f3ac610?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf811f3ac610</guid>
            <category><![CDATA[customer-satisfaction]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Wed, 03 Oct 2018 14:56:37 GMT</pubDate>
            <atom:updated>2018-11-16T14:01:00.254Z</atom:updated>
            <content:encoded><![CDATA[<p>La satisfaction client, souvent délaissée, est pour beaucoup synonyme de longs formulaires inutiles car n’ayant aucun impact pour les personnes qui font l’effort de les remplir. En même temps, si l’expression du client ne provoque aucun effet sur les projets ou sur les équipes qui se cachent derrière, nous ne pouvons pas leur donner tort.</p><p>Nous avons vu dans la satisfaction client un moyen de répondre à nos valeurs <a href="http://thetribe.io/valeurs.html">d’excellence et de progression continue</a>. Mesurer régulièrement la satisfaction client permet d’apporter une réponse rapide et sur-mesure à nos clients dans la relation que nous avons avec lui et sur ce que nous produisons, et ce, sans attendre la fin du projet.</p><p>Par quelles étapes sommes-nous passés pour recueillir la satisfaction et agir dessus ?</p><p>Ce qui nous semblait important dès le début de theTribe, c’est que la satisfaction des clients devait avoir un impact fort sur notre fonctionnement : on ne parle pas juste de formulaires à remplir, mais des questions qui vont changer positivement leur relation avec nous.</p><p>Par ailleurs, dès la création de theTribe, il y a eu une volonté de valoriser le travail des développeurs sur le plan humain mais également au niveau de sa rémunération. La satisfaction client a une influence directe <a href="http://thetribe.io/salaires-chez-thetribe">sur les primes</a>.</p><h3>Choix des questions</h3><p>La création des questionnaires a été l’étape la plus complexe du projet. L’idée était de sonder les clients en un minimum de questions. Alors même que nous venions de débuter l’aventure theTribe, nous avions déjà pu avoir un premier retour sur ce qui pouvait créer de la frustration chez nos clients :</p><ul><li>Le manque de communication ;</li><li>Un mauvais avancement du projet ;</li><li>Une mauvaise impression sur la qualité technique.</li></ul><p>Nous avions ainsi identifié nos questions, celles qui permettraient de pouvoir identifier un mécontentement ou un souci sur un projet, et d’y remédier. Étant continuellement à la quête de l’excellence, nous avons ajouté une question afin de savoir si nous réussissons à surprendre notre client, à créer un effet « Waow » chez lui.</p><p>Très vite, nous avons lancé nos questionnaires sur <a href="https://www.typeform.com/">TypeForm</a> et nous les avons transmis une fois par mois à nos clients en expliquant notre démarche. Puis, nous avons intégré la complétion du formulaire lors des rétrospectives de nos cérémonies SCRUM.</p><p>Avec les premiers retours, nous avons pu affiner nos formulations afin d’exploiter au mieux le ressenti des clients et diminuer l’effet de déjà-vu de ce genre de questionnaire.</p><p>Nous sommes arrivés aux questions suivantes :</p><ul><li>L’avancement est-il conforme à vos attentes ?</li><li>L’équipe à votre service est-elle réactive ?</li><li>Les efforts de la tribu ont-ils dépassé vos attentes ?</li><li>Quel est votre ressenti sur la qualité technique du projet ?</li><li>Avez-vous des questions ou des remarques ?</li></ul><p>Cela marchait plutôt bien quand nous avions un nombre de projets simultanés qui se comptait sur les doigts d’une main. L’équipe a grandi, le nombre de projets aussi, et le temps passé à étudier chaque réponse et calculer les notes était de plus en plus long.</p><p>La solution était simple : l’automatisation du process !</p><h3>Diffusion de l’information</h3><p>Fidèles à nos valeurs, il nous a semblé important que chaque membre de la tribu puisse connaitre l’évolution des autres projets, les problématiques rencontrées et puisse apporter des solutions. Il fallait qu’en plus du calcul automatique des notes, celles-ci soient communiquées à l’ensemble des membres de la tribu.</p><p>Nous avons fait le choix de diffuser un maximum d’informations entre les membres de la tribu via <a href="https://slack.com/">Slack</a>, l’ajout des retours de nos clients sur cet outil était la suite logique. En regardant de plus près l’API proposée, nous avions la possibilité d’aller un peu plus loin dans l’affichage en y intégrant nos règles d’intéressement pour les équipes.</p><p>Nous avons pour le moment trois possibilités, correspondant aux palliers de primes décidés en équipe, en sachant que la note maximum est 17 :</p><ul><li>Au-dessus ou égal à 13, l’équipe est récompensée pour ses bons résultats ;</li><li>Au-dessus ou égal à 10, l’équipe ne reçoit qu’une faible partie de la récompense ;</li><li>En dessous, aucune récompense n’est possible.</li></ul><p>Dans tous les cas, l’équipe devra se remettre en question pour s’améliorer et apporter une plus grande satisfaction (oui, même si elle est déjà optimale).</p><p>La diffusion de l’information était faite par un script, qui, en règle générale, était lancé quelques jours avant la fin du mois. Nous obtenons un message par projet, résumant les notes ainsi que le commentaire qui peut y être associé.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/392/1*301WozXp_JYcOfW9Qvpi9w.png" /></figure><p>Le résultat était satisfaisant et permettait de répondre au besoin que nous avions. A chaque fin de mois, il y avait un réel engouement lors de la découverte des notations effectuées par nos clients. Mais là encore, notre façon de procéder était encore perfectible et plusieurs problèmes se posaient chaque mois :</p><ul><li>L’avancement des notations n’était pas accessible à tout le monde ;</li><li>Il manquait régulièrement des réponses, il y avait plusieurs vagues de message sur Slack ;</li><li>Une seule personne dans l’entreprise était en capacité de déclencher l’export des notes sur Slack.</li></ul><h3>Le dashboard de la Satisfaction Client</h3><p>Nous avions besoin d’un tableau de bord nous permettant de voir où en étaient les notations, de modifier la liste des projets et surtout que tout le monde puisse demander à Hodor (notre bot Slack) la publication des notes.</p><p>Avec un savant mélange de <a href="https://getbootstrap.com/">Boostrap</a>, de <a href="https://silex.symfony.com/">Silex</a> et un soupçon de <a href="https://fr.wikipedia.org/wiki/Caf%C3%A9ine">Caféine</a>, nous avons créé une interface simple répondant à ces problématiques.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VDRnEyzfbLJOHN5sOKlOEw.png" /></figure><p>Tout les membres de la tribu peuvent consulter leur progression et celle des autres, ils peuvent également, s’ils le jugent nécessaire, publier les résultats.</p><p>La satisfaction de nos clients fait partie de la qualité de nos projets. Mais, comme tout processus, nous aurons à le maintenir pour correspondre à l’évolution des problématiques que nous rencontrerons afin de toujours nous améliorer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf811f3ac610" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/la-satisfaction-client-chez-thetribe-cf811f3ac610">La satisfaction client chez theTribe</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hackathon V2 — Hello Twitter !]]></title>
            <link>https://medium.com/thetribeio/hackathon-v2-hello-twitter-7e3acc3e1b1f?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/7e3acc3e1b1f</guid>
            <category><![CDATA[slack]]></category>
            <category><![CDATA[twitter]]></category>
            <category><![CDATA[bots]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Wed, 03 Oct 2018 14:07:10 GMT</pubDate>
            <atom:updated>2018-10-03T14:07:10.651Z</atom:updated>
            <content:encoded><![CDATA[<h3>Hackathon V2 — Hello Twitter !</h3><p>In this month hackathon, we are going to create a Twitter Slack bot to use the <a href="https://twitter.com/thetribeio">Twitter account of theTribe</a>.</p><h3>Project presentation and motivation</h3><ul><li>Hey man, have you seen this tweet? We should answer with the company’s Twitter!</li><li>You’re right, give me just a sec and I’ll do it.</li><li>You’ve got the access?</li><li>No, but I’ll ask the boss…</li></ul><p>A few moments later:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/584/1*5JK-SOqHrI4cSI6mlkSt7Q.png" /></figure><p>That’s the risk when you don’t have a communication department and you rely on people with a special sense of humor. Two problems emerged from this real and not fake at all story:</p><p>1) Everyone needs to have the credentials for the Twitter account. This is a bad practice and if someone leaves the company, the password must be changed.</p><p>2) We don’t want our beer lover developers to post anything when they have been drinking another good craft beer (social medias and alcohol don’t mix very well).</p><p>With this in mind, we decided to create a bot for our Slack team. This way no one needs to have the Twitter account credentials and we can decide all together if we want to publish a message or not. Another advantage of creating a bot is that we can capitalize on <a href="https://medium.com/thetribeio/our-first-hackathon-7f3d5924c017">the bot we created last time</a>.</p><h3>Conception</h3><p>We split up into two groups to work on the main features: the voting system and the tweet post.</p><h3>Vote system</h3><p>We agreed on a simple set of rules:</p><ul><li>People will vote by adding a 👍 or 👎 reaction on the suggested message</li><li>The bot will automatically add both reactions to the message to gain time (we don’t want to open the emoji modal everytime)</li><li>The message is posted if there are 5 more 👍 than 👎</li></ul><p>The bot needs to be aware of the reactions posted on the messages so we are using the two following events.</p><pre>start() {   <br>  super.start()<br>  ...   <br>  this.rtm.on(RTM_EVENTS.REACTION_ADDED,this.handleReaction)<br>  this.rtm.on(RTM_EVENTS.REACTION_REMOVED, this.handleReaction)<br>}</pre><p>This way we can retrieve the message on which the reaction has been added. Unfortunately, the Slack API only provides a light object as the message, without the attached list of reactions (how strange is this?), so we need to make another API call to retrieve this list.</p><pre>handleReaction({ item }) {<br>  this.checkNeedToPostMessage(item).then(isValid =&gt; {<br>    if (isValid) {<br>      // Post the message on Twitter<br>    }<br>  })<br>}</pre><pre>checkNeedToPostMessage({ channel, ts }) {<br>  return this.api.reactions.get({<br>      channel,<br>      timestamp: ts,<br>      full: true<br>  }).then(({ message: { reactions } }) =&gt; {<br>    return this.vote(reactions) &amp;&amp; this.checkAlreadyPosted(reactions);<br>  })<br>}</pre><p>Now, we can send a list of reactions to the vote function and implement the rules described above. The use of <a href="https://lodash.com/">lodash</a> makes it simple to read and write the code.</p><pre>vote(reactions) {   <br>  const plusReactions = _.find(reactions, { name: &#39;+1&#39; })   <br>  const minusReactions = _.find(reactions, { name: &#39;-1&#39; }) <br>   <br>  if (!plusReactions || !minusReactions) {     <br>    return false   <br>  }    </pre><pre>  return plusReactions.count - minusReactions.count &gt;= MIN_SCORE <br>}</pre><p>Now that we have a voting system, all we have to do next is to add 👍 and 👎 reactions to the new messages.</p><pre>start() {<br>  super.start()</pre><pre>  this.rtm.on(RTM_EVENTS.MESSAGE, ({ subtype, thread_ts, channel, ts, text, message }) =&gt; {<br>    if (subtype || thread_ts) {<br>      return<br>    }</pre><pre>  this.api.reactions.add(&#39;+1&#39;, { channel, timestamp: ts })<br>      .then(() =&gt; this.api.reactions.add(&#39;-1&#39;, { channel, timestamp: ts }))<br>  })<br>  ...<br>})</pre><p>Checking the subtype and the thread_ts variables allow us to differentiate messages in the original channel from edited messages or messages posted in a thread of the channel. We chained our promises to ensure that the reactions are always added in the same order.</p><h3>Sending a tweet</h3><p>Posting on Twitter is not really difficult once you installed the Twitter library:</p><pre>const Twitter = require(&#39;twitter&#39;);</pre><pre>class TweetBot extends Bot {<br>  constructor(...args) {<br>    super(...args)</pre><pre>  this.twitter = new Twitter({<br>      consumer_key: process.env.TWITTER_CONSUMER_KEY,<br>      consumer_secret: process.env.TWITTER_CONSUMER_SECRET,<br>      access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,<br>      access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET,<br>    });<br>  }<br>}</pre><p>In the constructor of our bot, we initialize the Twitter library so that it can use the team’s account to post the messages. Then we implement the sendTweet method:</p><pre>sendTweet({ text }) {<br>  const status = text.replace(/&lt;([^&gt;]*)&gt;/g, &#39;$1&#39;)<br>  return new Promise((resolve, reject) =&gt; {<br>    this.twitter.post(&#39;statuses/update&#39;, { status }, (error, tweets, response) =&gt; {<br>      if (error) {<br>        reject(error)<br>      } else {<br>        resolve(response)<br>      }<br>    })<br>  })<br>}</pre><p>In this method several things are done. First, we edit the Slack message, to remove &lt; and &gt; that surround links. Then as the post method is only accepting callbacks, we wrap the call in a promise, allowing us to catch errors and response more easily.</p><p>Now that our post method is ready, we can add it to the handleReaction method:</p><pre>handleReaction({ item }) {<br>  this.checkNeedToPostMessage(item).then(isValid =&gt; {<br>    if (isValid) {<br>      this.getMessage(item).then(message =&gt; {<br>        this.sendTweet(message)<br>      })<br>    }<br>  })<br>}</pre><pre>getMessage({ channel, ts }) {<br>  return this.api.channels.history(channel, {<br>    latest: ts,<br>    count: 1,<br>    inclusive: true<br>  }).then(({ messages }) =&gt; messages[0])<br>}</pre><p>Here we need another method to get the message, as the Slack API doesn’t send the content of the message on the Reaction events. To retrieve a message, we need to call the history method that will send us back messages that correspond to criterias. To get a particular message, you need to pass the timestamp of the message as the latest timestamp, add the inclusive: true, and of course take the first element of the array.</p><p>One last thing to do before posting a message is checking that the message hasn’t already been posted. Let’s assume for now that the bot adds the <em>:white_check_mark:</em> or the <em>:x:</em> emoji when the message is posted. With this in mind, we can easily know if the message has been posted.</p><pre>checkAlreadyPosted(reactions) {<br>  return _.some(reactions, ({ name, users }) =&gt; (name === &#39;white_check_mark&#39; || name === &#39;x&#39;) &amp;&amp; users.includes(this.rtm.activeUserId))<br>}</pre><p>Here thanks to lodash, we check in the message reactions if the aforementioned emoji has been added by the bot.</p><h3>Errors and message length</h3><p>There are several errors to check and informations to display. We decided to managed only three cases: the success, the unknown error and the message length error. As we wrapped the post method in a promise, we can now manage success and errors in a simple fashion.</p><pre>handleReaction({ item }) {<br>  ...<br>  this.sendTweet(message).then(() =&gt; this.addSuccessReaction(item), () =&gt; this.addErrorReaction(item))<br>  ...<br>}</pre><pre>addSuccessReaction({ channel, ts }) {<br>  return this.api.reactions.add(&#39;white_check_mark&#39;, { channel, timestamp: ts })<br>}</pre><pre>addErrorReaction({ channel, ts }) {<br>  return this.api.reactions.add(&#39;x&#39;, { channel, timestamp: ts })<br>}</pre><p>This way, we add a check mark icon when the message is posted, or a red cross if a problem occurred while posting.</p><p>We don’t need to post the tweet to know the message is too long, so we created a method to check the message length and updated the method linked to the messages creation or update.</p><pre>start() {<br>  super.start()</pre><pre>this.rtm.on(RTM_EVENTS.MESSAGE, ({ subtype, thread_ts, channel, ts, text, message }) =&gt; {<br>    if (subtype === &#39;message_changed&#39;) {<br>      if (this.sizeOf(message.text) &gt; MAX_SIZE) {<br>        this.api.reactions.add(&#39;no_entry_sign&#39;, { channel, timestamp: message.ts })<br>      } else {<br>        this.api.reactions.remove(&#39;no_entry_sign&#39;, { channel, timestamp: message.ts })<br>      }<br>    }<br>    if (subtype || thread_ts) {<br>      return<br>    }</pre><pre>this.api.reactions.add(&#39;+1&#39;, { channel, timestamp: ts })<br>      .then(() =&gt; this.api.reactions.add(&#39;-1&#39;, { channel, timestamp: ts }))<br>      .then(() =&gt; {<br>        if (this.sizeOf(text) &gt; MAX_SIZE) {<br>          this.api.reactions.add(&#39;no_entry_sign&#39;, { channel, timestamp: ts })<br>        }<br>      })<br>  })<br>  ...<br>})</pre><pre>sizeOf(message) {<br>  const urlRegex = /&lt;https?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?&gt;/g;<br>  return message.replace(urlRegex, &#39; &#39;.repeat(23)).length<br>}</pre><p>With this we can display an emoji showing that the text is too long. To understand the sizeOf method, you must know that Twitter automatically minifies the urls to a 23-character-long url.</p><h3>Conclusion</h3><p>Well, a picture is worth a thousand words:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/203/1*IEGhvBVt87S4sdwNeIPkBw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/269/1*JwA-vDegmHMXZ4iMzqMhJQ.png" /></figure><p>During this hackathon, we managed to create a simple Slack bot that posts messages on Twitter. This first version is capable of tweeting text, can count votes and publish the message, and warns us if the message is too long or if there has been an error.</p><p>This bot is really simple and could be upgraded to send images, retweet or even post on other social medias. Theses features will likely be the subject of another hackathon, so stay tuned for more!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7e3acc3e1b1f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/hackathon-v2-hello-twitter-7e3acc3e1b1f">Hackathon V2 — Hello Twitter !</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hello world!]]></title>
            <link>https://medium.com/thetribeio/hello-world-91dea98752cf?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/91dea98752cf</guid>
            <category><![CDATA[the-tribe]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Wed, 25 Jul 2018 15:11:27 GMT</pubDate>
            <atom:updated>2018-07-25T15:11:27.944Z</atom:updated>
            <content:encoded><![CDATA[<p>Parlons un peu de notre blog, voulez-vous ?(english version below)</p><h3>Mais vous êtes qui ?</h3><p>Nous, nous sommes les sioux de <a href="http://thetribe.io/">theTribe</a>, des développeurs, des aficionados du web et des choses bien faites. Nous sommes une grande famille qui s’est réunie autour de <a href="http://thetribe.io/#developers">valeurs communes</a> telles que le partage, l’entraide, l’excellence technique, mais bien sûr autour d’une passion commune pour le web. Nous développons principalement avec Symfony, React, Node.js, Angular, mais nous cherchons toujours de nouvelles technos à expérimenter.</p><h3>Ce blog, y aura quoi dedans ?</h3><p>Ce blog va nous permettre de nous exprimer sur plusieurs thèmes :</p><ul><li>Les sujets et problématiques techniques que nous avons pu rencontrer dans nos différents projets</li><li>La vie de développeur chez <a href="http://thetribe.io/">theTribe</a>, avec des présentations de technos, d’outils ou de méthodes que nous mettons en place</li><li><a href="https://http.cat/">Les chats</a></li><li>Les hackathons que nous faisons de temps en temps</li><li>Les revues de presse d’articles que nous avons trouvés intéressants ou instructifs</li><li>Des retours de conférences/talks</li></ul><h3>Plutôt français ou anglais ?</h3><p>Tout dépend de la technicité de l’article, de sa visée, de son auteur… En bref, pas de règle précise, il se peut que certains articles soient disponibles dans les deux langues (un peu comme celui-ci).</p><h3>Bonne lecture !</h3><p>Hello my English speaking friend, let’s talk about this blog shall we?</p><h3>Who are you?</h3><p>We are developers from <a href="http://thetribe.io/">theTribe</a>, Internet and well-crafted-thing lovers. We are a big family gathered around common values such as sharing, helping each other, being technically excellent, and of course around our love for the web. We are mainly using Symfony, React, Node.js and Angular to develop, but we are always looking for new technologies to test.</p><h3>What’s inside?</h3><p>We will write about several topics:</p><ul><li>Technical Problems we came across and solutions we crafted on our projects</li><li>Our life as developers at <a href="http://thetribe.io/">theTribe</a>: technologies, tools or methods we are using</li><li><a href="https://http.cat/">Cats</a></li><li>The hackathons we regularly do</li><li>Press reviews of articles we thought of interest or instructive</li></ul><h3>English or French?</h3><p>No precise rule here, it depends on the technical nature of the article, its aim, the author… Some articles might be available in both languages (like this one).</p><h3>Good reading!</h3><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=91dea98752cf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/hello-world-91dea98752cf">Hello world!</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our first Hackathon]]></title>
            <link>https://medium.com/thetribeio/our-first-hackathon-7f3d5924c017?source=rss-9d4a9a6a01d------2</link>
            <guid isPermaLink="false">https://medium.com/p/7f3d5924c017</guid>
            <category><![CDATA[slackbot]]></category>
            <category><![CDATA[raspberry-pi]]></category>
            <category><![CDATA[slack]]></category>
            <dc:creator><![CDATA[theTribe]]></dc:creator>
            <pubDate>Wed, 25 Jul 2018 14:57:54 GMT</pubDate>
            <atom:updated>2018-07-25T15:04:24.038Z</atom:updated>
            <content:encoded><![CDATA[<p>One of the extra professional activities we started here at theTribe is the organisation of small internal events, scheduled on a week evening, that we called “Hackathon”. The principle is simple: we gather after work, begin a simple and short project, with some pizzas and beers (very important ;) ), and we share about our practices to produce something that works before going home. These sessions have multiple beneficial effects: the project aims to simplify our every day life, it is an occasion to test new technologies and frameworks, and the team spirit is boosted as everybody work on the same project with everyone creativity.</p><h3>Who the hell will unlock the door ?</h3><p>As we moved into our new office, we realized we didn’t have enough keys for everyone in the team, for the entrance of the building. When you don’t have the key, you have to call on the intercom to get the door open. Furthermore, our landlord isn’t a big fan of having 25 keys for a single office, so we gave the keys to the guys who begin early, and the rest of us just had to ring the bell. Easy, simple, no problem. Until the two guys with the keys are late or sick or someone gets mad because of the continuous intercom ringing between 9am and 10am. So we decided to take care of this by linking the intercom to our Slack team in order to be able to open the door from outside the office (or when we are too lazy to move 10 meters to open the door). Here was the subject for our first Hackathon session.</p><h3>Software</h3><p>The easiest and more secure way to handle the intercom via Internet for us was to connect a bot on the local network to Slack, and control this bot via Slack. Thus, the bot is isolated from the global network and cannot be hacked directly. This solution demands of course to trust Slack, and we made this choice willingly.</p><p>The first step was therefore to create a Slack bot user and to connect it to our team. Slack offers a relatively simple service for that (<a href="https://api.slack.com/bot-users">see documentation here</a>); once the bot user created, it can be accessed directly from our code via Slack’s Real Time Messaging API (RTM API). Once connected, the code can intercept every message posted on the channel to trigger a callback.</p><h3>The code</h3><p>We decided to use NodeJS to program the bot, because it is rather simple to set up and the RTM API can be used quite simply with a single package.</p><p>The base code for connecting the bot to slack and listening to the events is the following:</p><pre>var RtmClient = require(&#39;@slack/client&#39;).RtmClient; <br>var RTM_EVENTS = require(&#39;@slack/client&#39;).RTM_EVENTS;  </pre><pre>rtm.start();  </pre><pre>rtm.on(RTM_EVENTS.MESSAGE, function(message) {   <br>  // Callback triggered every time a message is sent to a channel    where   <br>  // the bot is present or directly to the bot via private message <br>});</pre><p>The second problem was to connect the bot to the Raspberry Pi’s pins, in order to send electrical signals to the relay and then open the door (see <strong>Hardware</strong>).</p><p>Fortunately, a library has been developed just to control the pins! It also works with events, and it’s rather simple to use too:</p><pre>var gpio = require(&#39;rpi-gpio&#39;);  </pre><pre>// Set up a channel for read or write <br>gpio.setup(channel [, direction, edge], callback);  </pre><pre>// Write the value of a channel <br>gpio.write(channel, value, callback);</pre><p>With these elementary bricks, it was then easy to write the code to open the door, and with a bit of debug, to make it work.</p><pre>require(&#39;dotenv&#39;).load();  </pre><pre>var _ = require(&#39;lodash&#39;); <br>var gpio = require(&#39;rpi-gpio&#39;); <br>var RtmClient = require(&#39;@slack/client&#39;).RtmClient; <br>var RTM_EVENTS = require(&#39;@slack/client&#39;).RTM_EVENTS;<br>  <br>var rtm = new RtmClient(process.env.SLACK_API_TOKEN, { logLevel: &#39;warning&#39; }); <br> <br>var PIN_BUTTON = 6; // Arbitrary pin connected to the opening button var PUSH_BUTTON_DELAY = 3000; // Delay during which the opening signal is sent  </pre><pre>rtm.start(); <br> <br>var lock = false;  </pre><pre>function openTheDoor(cb) {<br>  if (lock) return;<br>  lock = true;<br>  gpio.setup(PIN_BUTTON, gpio.DIR_OUT, function() {<br>    gpio.write(PIN_BUTTON, false, function(err) {<br>      if (err) throw err;<br>      setTimeout(function() {<br>        gpio.write(PIN_BUTTON, true, function(error) {<br>          if (error) throw error;<br>          lock = false;<br>          if (cb) cb();<br>        });<br>      }, PUSH_BUTTON_DELAY);<br>    });<br>  });<br>}</pre><pre>// Chat bot<br>rtm.on(RTM_EVENTS.MESSAGE, function(message) {<br>  function contains(needles) {<br>    return message.text &amp;&amp; _.every(needles, function(needle) {<br>      return message.text.toLowerCase().indexOf(needle.toLowerCase()) !== -1;<br>    });<br>  }</pre><pre>if (contains([&#39;&lt;@&#39; + rtm.activeUserId + &#39;&gt;&#39;, &#39;open&#39;])) {<br>    rtm.sendMessage(&#39;I will now open the door!&#39;, message.channel);<br>    openTheDoor(function() {<br>      rtm.sendMessage(&#39;The door is opened&#39;, message.channel);<br>    });<br>  }<br>});</pre><h3>Service</h3><p>The last step was to encapsulate the bot in a service to run as a background process and, ideally, boot on startup in case of power cut.</p><p>Since our Raspbian (a Debian-based OS optimized for Raspberry Pi) uses Systemd to manage the services, we configured a new service named thedoor as follows:</p><pre>[Unit] <br>Description=theDoor <br>After=network.target  </pre><pre>[Service] <br>Type=simple <br>ExecStart=/usr/bin/nodejs /home/pi/thedoor/index.js <br>Restart=always  </pre><pre>[Install] <br>WantedBy=default.target</pre><p>Finally, we configured it as a service, which needs to be activated with systemctl enable thedoor.</p><h3>Hardware</h3><p>First, we had to check how our intercom is working in order to know what we can do with it. We looked up for electrical plans on the Internet and found this wiring diagram:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/394/1*9MdGBVlQx_ljCYxJkZR19w.png" /></figure><p>Thanks to that, we know that when we are pressing the opening button, it creates a contact allowing the current to pass through the door-opening system. All we have to do is connect the two parts linked to the button to open the door whenever we want.</p><p>To do so, we need to isolate the control system (the Raspberry) from the active system (the intercom) because they do not work under the same voltage and amperage. We selected the following relay to do the job:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/350/1*Idv8QwXzA9d0of1XW2Fydw.jpeg" /></figure><p>The commutation voltage is around 3V and can handle a lot more than the 15V we have in the door-opening circuit. This relay allows us to close the door-opening circuit when we have 5V on an out pin of the Raspberry. Moreover, having a spare relay on the component allows us to add other features to the system if need be.</p><p>With everything in mind, we came up with this simple wiring diagram for our project:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/462/1*KWdA1DeWYO0JA0l9PLGGmA.png" /></figure><p>The button and the relay are wired in parallel, allowing us to use both our Raspberry and the intercom button to close the circuit and open the door.</p><p>After some welding, we connected our system to the intercom:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/350/1*O3msjkFsO7c1aqP-jaQkjw.jpeg" /></figure><p>The screws used to connect the button to the door-opening circuit make it easy for us to add our system without damaging the intercom (We don’t want to upset our landlord, do we?).</p><h3>Conclusion</h3><p>It took us 4 hours for an approximate cost of 60€, but the result is here, we can now open the door thanks to our smartphones with a simple message on Slack! No more ringing bell in the morning, welcome Slack notifications!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7f3d5924c017" width="1" height="1" alt=""><hr><p><a href="https://medium.com/thetribeio/our-first-hackathon-7f3d5924c017">Our first Hackathon</a> was originally published in <a href="https://medium.com/thetribeio">theTribe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>