Add person-to-person direct payments

To enable someone to make a payment to another person via the bot, complete these tasks:

  • Create a listener to enable a user to bind their PayPal email address with the Slack ID, to be used for payment.
  • Create a listener to hear the pay command from a user.
  • Create an interactive callback to start the payment processing.
  • Create web server endpoints to enable for finalizing the payment once the user returns from PayPal, and for handling cancellations during the payment.

Listen for the PayPal email bind command

To make person-to-person payments through the Slack bot, bind the users' PayPal email addresses to their Slack IDs. Then, you extract emails from the bot to process payments. To do so, configure the bot to listen for 'add paypal ' commands to add emails. For example, add paypal myemail@email.com. With this method, users do not need to know another person's PayPal email to start payment.

/****************************************************************
* Attach a PayPal payment email to a Slack user ID, for payment
****************************************************************/
controller.hears(['add paypal (.*)'], 'direct_message,direct_mention,mention', function(bot, message) {
  var email = message.match[1];
  controller.storage.users.get(message.user, function(err, user) {
    // Create new user if one does not exist
    if (!user) {
      user = {
        id: message.user,
      };
    }
    // Extract single email from mailto string and store
    // Source: http://www.regular-expressions.info/email.html
    user.email = email.match(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/)[0];
    
    // Save user record
    controller.storage.users.save(user, function(err, id) {
      bot.reply(message, 'Your PayPal payment email will be ' + user.email + ' from now on.');
    });
  });
});

Extract the email and search the bot storage to determine whether the user already exists. If not, add them. Then, use regex to extract the email address because the email is in an extended form on Slack. Finally, add that email to the user record and display a bot reply.

Listen for the pay user command

When a user sends a pay command for a specific user as a direct message to your bot, you must set up the controller to hear those messages.

/****************************************************************
* If user has requested a direct user payment, provide a
* confirmation before pushing to callback for processing
****************************************************************/
controller.hears(['pay (.*) (.*) (.*)'], 'direct_message', function(bot, message) {
  var msg = message.match[0];
  var uid = message.match[1].replace(/\W+/g, '');
  var amount = message.match[2];
  var currency = message.match[3];
  
  bot.reply(message, {
    attachments: [{
      title: 'Would you like to ' + msg + '?',
      callback_id: '222',
      attachment_type: 'default',
      actions: [{
        name": 'yes',
        text": 'Yes',
        value": uid + '|' + amount + '|' + currency,
        type": 'button',
      },{
        name:'no',
        text: 'No',
        value: 'no',
        type: 'button',
      }]
    }]
  });
});

To make a payment to a payee using a direct message to the bot, use the hears method to listen for the command. For example, the payer will type 'pay @USER 10 USD', which will display yes/no buttons for the payer to begin payment to the payee. Extract the user ID, amount, and currency, then send those values along with the yes button, if clicked.

Create the interaction callback to start payment

Once the user clicks on the yes or no buttons, you need to handle that active through the interactive message callback. If no was clicked, simply cancel out. If yes was clicked, create a link to go to PayPal to begin the payment.

/****************************************************************
* Handle all payment requests in callback, once the user has 
* selected that they want to process a payment / subscription
****************************************************************/
controller.on('interactive_message_callback', function(bot, message) {
  // If user clicks no for payment, cancel, otherwise process payment
  if (message.actions[0].value === 'no') {
    bot.replyInteractive(message, 'Payment process cancelled');
  } else {
    bot.replyInteractive(message, 'Generating your payment link. Hold on a moment...');
    
    // Prepare base payment message for bot to respond with
    var reply = {
      attachments: [{
        fallback: 'Payment initiation information failed to load',
        color: '#36a64f',
        pretext: 'Click the link below to initiate payment',
        title: 'Make payment to COMPANY',
        footer: 'PayPal Payment Bot',
        footer_icon: 'https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-08-17/70252203425_a7e48673014756aad9a5_96.jpg',
        ts: message.ts
      }]
    };
  
    var dataArr = message.actions[0].value.split('|');
        
    controller.storage.users.get(dataArr[0], function(err, user) {
      if (!user.email) {
        bot.replyInteractive(message, 'User has no PayPal email associated with their account. Please ask them to add one with the command "add paypal EMAIL"');
      }
          
      // Build PayPal payment request
      var payReq = JSON.stringify({
        intent: 'sale',
        redirect_urls: {
          return_url: redirect + '/process',
          cancel_url: redirect + '/cancel'
        },
        payer: {
          payment_method: 'paypal'
        },
        transactions: [{
          description: 'This is the payment transaction description.',
          amount: {
            total: dataArr[1],
            currency: dataArr[2]
          },
          payee: {
            email: user.email
          }
        }]
      });
        
      // Create payment request before PayPal redirect to approve
      paypal.payment.create(payReq, function(error, payment) {
        if(error) {
          console.error(JSON.stringify(error));
        } else {
          // Capture HATEOAS links
          var links = {};
          payment.links.forEach(function(linkObj) {
            links[linkObj.rel] = {
              href: linkObj.href,
              method: linkObj.method
            };
          })
    
          // If redirect url present, insert link into bot message and display
          if (links.hasOwnProperty('approval_url')) {
            reply.attachments[0].title_link = links['approval_url'].href;
            bot.replyInteractive(message, reply);
          } else {
            console.error('no redirect URI present');
          }
        }
      });
    });
  }
});

There are a few steps you go through in the above code, once the user clicks the yes button:

  • Create an interactive reply immediately to let the user know that the processing has started, so that they don't think that something is going wrong.
  • Create the interactive reply that will house the link to PayPal, since you can't automatically redirect the user to a web address.
  • Next, create the payment request object, including all payment information needed.
  • Call payment.create(...), passing in the payment request object, to start the payment process and retrieve a PayPal redirect link.
  • If the payment is started successfully, extract the approval URL (where you need to forward the user to), add it to your bot reply, and then display the reply to the user.

Create an endpoint to finalize payment

After the user confirms payment, he or she is redirected to your app to the /process endpoint. To handle this redirect, your app must add a /process HTTP GET endpoint.

/****************************************************************
* Process a direct PayPal payment once user is redirected back 
****************************************************************/
controller.webserver.get('/process', function(req, res) {
  // Extract payment confirmation information needed to process payment
  var paymentId = req.query.paymentId;
  var payerId = { payer_id: req.query.PayerID };

  // Attempt to complete the payment for the user
  paypal.payment.execute(paymentId, payerId, function(error, payment) {
    if(error) {
      console.error(JSON.stringify(error));
    } else {
      if (payment.state == 'approved') { 
        res.send('Payment completed successfully');
      } else {
        res.send('Payment not successful');
      }
    }
  });
});

Once that endpoint is triggered, extract the payment ID and payer ID that is sent from PayPal. Once extracted, call payment.execute(...), passing in those variable, to complete payment.

Create an endpoint to handle payment cancellations

If the customer cancels during the PayPal payment confirmation step, he or she will be redirected back to your application, to the /cancel endpoint. To handle this redirect you need to add a /cancel HTTP GET endpoint.

/****************************************************************
* Payment incomplete: User cancelled the transaction on PayPal
****************************************************************/
controller.webserver.get('/cancel', function(req, res) {
  res.send('Payment cancelled');
});

This example simply displays a payment canceled message, but your app should provide a properly handled experience.