Customer feedback was always in the back of my mind when I set out to build Forest Requirements and Forest Requirements Professional for the iPad. I wanted a way for customers to be able to easily get in touch with me – whether it was to provide feedback, suggestions, bug reports or even to complain. I put together a quick and dirty solution using Node.js and Gmail that has served me (and my customers) very well, and this is how I built it.

Making it easy to use

I’ve seen some pretty rough customer feedback systems in mobile apps before, and I wanted my system to be as easy to use as possible. It’s accessible from every screen within Forest Requirements:

I tried hard to keep the feedback form simple – most fields are optional, which I feel is a friendlier user experience (though with that said, every customer that has contacted me through this system has provided full contact details). This is what the feedback form looks like:

Sending feedback to the server

The client-side code for submitting feedback is fairly straightforward. In short, the client POSTs JSON to a Node.js endpoint on the server. At a high level, this is what the feedback submission code looks like:


#define kFeedbackUrl            @"http://www.myserver.com/api/feedback"

#define kTimestampKeyName       @"timestampKey"
#define kIOSVersionKeyName      @"iOSVersionKey"
#define kTypeKeyName            @"typeKey"
#define kCommentsKeyName        @"messageKey"
#define kEmailKeyName           @"emailKey"
#define kNameKeyName            @"nameKey"

#define kFeedbackType           @"feedback"

@interface FeedbackManager : NSObject{
@private
    dispatch_queue_t feedbackManagerOpQueue;
}

+(id)sharedInstance;
-(void)submitFeedback:(NSString*)comments fromUser:(NSString*)name withEmail:(NSString*)email andCallback:(void (^)(bool))callback;
@end

The feedback submission system is a singleton object that processes feedback in the background. Besides defining the class itself, I’ve defined an endpoint, a few keys that will end up in the JSON blob we send to the server, and a Grand Central Dispatch queue for executing feedback POST operations.

Here’s the implementation:

#import "FeedbackManager.h"
#import "ConnectivityHelper.h"
#import "SBJson.h"

@interface FeedbackManager ()
+(void)submitFeedbackData:(NSString*)jsonData withCallback:(void (^)(bool))callback;
+(NSString*)getTimestamp;
+(NSString*)convertDateToISO8601:(NSDate*)date;
@end

@implementation FeedbackManager

#pragma mark - Public Facing API

+ (id)sharedInstance
{
    static dispatch_once_t pred = 0;
    __strong static id _sharedObject = nil;
    dispatch_once(&pred, ^{
        _sharedObject = [[self alloc] init];
    });
    return _sharedObject;
}

-(id)init
{
    self = [super init];
    if (self) {
        feedbackManagerOpQueue = dispatch_queue_create("com.forest.FeedbackManagerOperationQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

-(void)submitFeedback:(NSString*)comments fromUser:(NSString*)name withEmail:(NSString*)email andCallback:(void (^)(bool))callback
{
    if (email == nil)
        email = @"noemail@myserver.com";
    if ([email length] == 0)
        email = @"noemail@myserver.com";

    dispatch_async(feedbackManagerOpQueue, ^{
        NSMutableDictionary* data = [[NSMutableDictionary alloc] init];
        [data setObject:[FeedbackManager getTimestamp] forKey:kTimestampKeyName];
        [data setObject:[[UIDevice currentDevice] systemVersion] forKey:kIOSVersionKeyName];
        [data setObject:kFeedbackType forKey:kTypeKeyName];
        [data setObject:comments forKey:kCommentsKeyName];
        [data setObject:name forKey:kNameKeyName];
        [data setObject:email forKey:kEmailKeyName];

        NSString* jsonData = [data JSONRepresentation];
        [FeedbackManager submitFeedbackData:jsonData withCallback:callback];
    });
}

#pragma mark - Submit Feedback Support

+(void)submitFeedbackData:(NSString*)jsonData withCallback:(void (^)(bool))callback
{
    // Check for connectivity.
    if (![ConnectivityHelper hasConnectivity])
        return;

    // Create request data.
    NSData* requestData = [jsonData dataUsingEncoding:NSUTF8StringEncoding];

    // Create URL to POST jsonData to.
    NSURL* url = [NSURL URLWithString:kFeedbackUrl];

    // Create request.
    NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody: requestData];

    // Send request synchronously.
    NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] init];
    NSError* error = nil;
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

    // Check result.
    if (error == nil)
    {
        int statusCode = [response statusCode];
        if (statusCode == 200)
            dispatch_async(dispatch_get_main_queue(), ^{ callback(YES); });
        else
            dispatch_async(dispatch_get_main_queue(), ^{ callback(NO); });
    }
    else {
        dispatch_async(dispatch_get_main_queue(), ^{ callback(NO); });
    }
}

+(NSString*)getTimestamp
{
    NSDate* now = [[NSDate alloc] init];
    NSString* timestamp = [FeedbackManager convertDateToISO8601:now];
    return timestamp;
}

+(NSString*)convertDateToISO8601:(NSDate*)date;
{
    static NSDateFormatter* sISO8601 = nil;

    if (!sISO8601) {
        sISO8601 = [[NSDateFormatter alloc] init];
        [sISO8601 setTimeStyle:NSDateFormatterFullStyle];

        NSTimeZone *gmt = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
        [sISO8601 setTimeZone:gmt];

        NSMutableString *strFormat = [NSMutableString stringWithString:@"yyyy-MM-dd'T'HH:mm:ss'+00:00'"];
        [sISO8601 setDateFormat:strFormat];
    }
    return[sISO8601 stringFromDate:date];
}

@end

The feedback submission system puts together an NSDictionary of the data the user entered; that dictionary is then converted to JSON via SBJson. I happen to use a specific format for timestamps, so the code also converts the timestamp of the submission to a NSString in ISO 8601 format. Additionally, I put in a dummy email address as a placeholder if the user declines to enter an email address.

Using the feedback system couldn’t be easier – here’s an example:


[[FeedbackManager sharedInstance] submitFeedback:@"my comments" fromUser:@"Rob Ringham" withEmail:@"myemail@address.com" andCallback^(bool success) {
    [self handleFeedbackResponse:success]
}

A quick call to the singleton FeedbackManager object sends the feedback on its way; a callback to the UI handles the result of the operation (since the actual POST of the data happens on a GCD serial queue in the background).

Handling feedback from the client

Processing the feedback on the server is easy – for Forest Requirements, I use a combination of Node.js, Express, SimpleDB (for archiving feedback), node-mail and Gmail. I won’t get into the details of how I use SimpleDB, but I’ll go over how I receive feedback and email it to myself – it’s simple and it has been working great for several weeks now.

First, here is the JSON blob that the server receives:

{
  "typeKey": "feedback",
  "iOSVersionKey": "5.1",
  "messageKey": "Here is my feedback!",
  "timestampKey": "2012-07-29T15:59:13+00:00",
  "emailKey": "myemail@address.com",
  "nameKey": "Ringham, Rob"
}

Here is how the server handles a feedback POST:

var forestMail = require('./forestMail');

app.post('/feedback', function(req, res) {
    if (req.body.typeKey == 'feedback') {
        forestMail.mailFeedback(req.body);
    }
    else if (req.body.typeKey == 'suggestion') {
        forestMail.mailSuggestion(req.body);
    }
    else if (req.body.typeKey == 'support') {
        forestMail.mailSupport(req.body);
    }

    res.send(200);
});

Pretty straightforward. Once it’s received, I then email it to myself using the fantastic node-mail module. This is what it looks like:

var mailer = require('mail');

var emailSource = 'feedbackdestination@gmail.com';
var emailDest = 'feedbackdestination+feedback@gmail.com';
var emailPass = 'passwordForGmailAccount';
var emailHost = 'smtp.gmail.com';

var mail = mailer.Mail({
 host : emailHost,
 username : emailSource,
 password : emailPass
});

module.exports = {

  mailFeedback: function (body) {
      var mailMessage = 'Feedback from ' + body.nameKey + '\n\n' +
                        'From: ' + body.nameKey + '\n' +
                        'Email: ' + body.emailKey + '\n' +
                        'Timestamp: ' + body.timestampKey + '\n' +
                        'iOS Version: ' + body.iOSVersionKey + '\n\n' +
                        'Message:' + '\n\n' +
                         body.messageKey;

      mail.message({
          from: "supportdispatch@forestrequirements.com",
          to: emailDest,
          subject: 'Feedback from ' + body.nameKey,
      })
      .body(mailMessage)
      .send(function(err) {
          if (err) {
              console.log('error sending email: ' + err);
          }
      });
  },

  mailSuggestion: function (body) {
      // Same as mailFeedback, just uses a different email template.
  },

  mailSupport: function (body) {
      // Same as mailFeedback, just uses a different email template.
  }

};

Gmail is great for this – it’s quick and easy to set up and it incredibly convenient to manage. Additionally, using Gmail’s ‘+’ syntax, I can easily set up filters to categorize customer emails.

Supporting my customers

I’d argue that building a feedback system into an app is the easiest part of this type of customer service. Each customer that takes the time to contact you deserves a reply – even if it’s as simple as a personal thank you note, thanking a customer for using the app and offering their feedback.

I take the feedback I receive very seriously – especially suggestions from customers. I try to engage each customer who suggests a feature to find out what it is they really need – and the process is not always as straightforward as building exactly what they suggest.

Supporting customers takes time and hard work, but I believe it will pay off in the end – both in terms of happy customers and a better product.

Follow

Get every new post delivered to your Inbox.