Getting additional facebook permissions in ios app

Hello there!

I’m writing an App, which makes posts on users’ behalf. To achieve this, we need to require additional permission from Facebook – publish_actions.

As per Facebook’s guideline, we should ask for additional permissions at the time we need it, not at the time of first login. So we do it in the middle of our user’s workflow. We use facebook-sdk package to enable FB api.

We do it like this:

FB.login(function (response) {
 if (response.authResponse) {
  var scopes = response.authResponse.grantedScopes;
  if (scopes.indexOf('publish_actions') == -1) {
   //save that user declined and never ask again
  } else {
   // access granted!
   // do stuff
  }
 } else {
  // no response -- assume decline
 }
 //do stuff
}, {scope: 'publish_actions', return_scopes: true});

Everything works fine, but on the iOS platform this triggers opening popup window, which is not closed. And the user sees blank white window. How to open it in current window or in iframe or i don’t know what to do… ) How do they do it in accounts-facebook?

going to use approach from here: http://stackoverflow.com/questions/7205987/facebook-login-with-redirect
hope it would help )

So, we managed to do that!

The problem was that calling FB.login opens popup window, which is not closed in webView. So we moved to redirect scheme. Also, we worked with a few more problems… se below )

First, we identified places in users’ workflow, where we wanted to create posts for them. Second, we carefully redesigned workflow in such a way, that after request for permissions there is some accessible by direct link endpoint, to which we can redirect a user.

So the code is as follows:
In lib/util.js

Meteor.util.hashToURL = function (base, hash) {
	var arr = [];
	for (var i in hash)
		arr.push(i + '=' + encodeURIComponent(hash[i]));
	ret = arr.join('&');
	if (!base)
		return ret;
	return base + '?' + ret;
}

//object is an object for which we want to make a post -- it can be a sort of wine, or runaway distance...
//type is a type of custom story. tasted, run, read.
//redirect is endpoint, where we get after fb interaction
Meteor.util.shareInterface = function (object, type, redirect) {
        //some code to determine if we should request for permissions, show modal windows, etc..
        if (permissionsCanBeGranted) {
		var base = 'http://meteor.local/fb/auth';  //use meteor.local for iOS app
                //we provide params as GET parameters, because after redirect Session would be clean
		var backUrl = Meteor.util.hashToURL(base, {
			fb_redirect: redirect,
			fb_object: object._id,
			fb_type: type
		});
		var params = {
			client_id: Meteor.util.cnst.fbAppId,
			response_type: 'code granted_scopes', //return permissions!
			scope: 'publish_actions', //make posts on user's behalf
			redirect_uri: backUrl
		};
		
		var fbUrl = Meteor.util.hashToURL('https://www.facebook.com/dialog/oauth', params);
		top.location.href = fbUrl;
        }
}

In lib/routes.coffee we handle redirect from FB

Router.map ->
  @route '/fb/auth', ->
    @render 'wait' //some loader template, just in case
    Meteor.util.shareFBCallback()

Actual code is hidden in lib/util.js. So, there again

Meteor.util.shareFBCallback = function() {
	var get = Router.current().location.get().queryObject;
	var scopes = get.granted_scopes;
	var redirect = get.fb_redirect;
	console.log('[Meteor.util.shareFBCallback] get =');
	console.log(get);
	if (!redirect) {
		Router.go('/lenta');
		return;
	}
	if (scopes.indexOf('publish_actions') != -1) {
		// granted!
		var objectId = get.fb_object;
		var object = {_id: objectId};
		var type = get.fb_type;
		console.log('[Meteor.util.shareFBCallback] access granted, objectId ' + objectId);
		
		Meteor.util.sharePost(object, type, function () {
			Router.go(redirect);
		});
		return;
	}
        //auxilary method to save permissions decline to user's profile
	Meteor.util.saveShareDecline();
	Router.go(redirect);
}

Meteor.util.sharePost = function (object, type, cb) {
	if (!type)
		type = 'defaultType';
	FB.api(
	  'me/MYNAMESPACE:' + type,
	  'post',
	  {
	  	// aux method to get static object page url -- see below
	    object: Meteor.util.getObjectStaticPageUrl(object)
	  },
	  function(response) {
	  	console.log('FB share post response');
	  	console.log(response);
	  	cb();
	  }
	);
}

To make a custom story, FB’s bot, as for now, require some OpenGraph tags on a page. Also, as for now, in Meteor we have no way to control head tag’s contents. So we devised a clever way – server route which generates some ‘static html’ pages specially for FB bot.

In lib/util.js

//object's link for FB bot
Meteor.util.getObjectStaticPageUrl = function (object) {
	return Meteor.absoluteUrl() + 'static_objects/' + object._id;
}

//object's link in a real living system
Meteor.util.getObjectPageUrl = function (object) {
	return Meteor.absoluteUrl() + 'object/' + object._id;
}

Meteor.util.getStaticObjectPageHtml = function(object) {
  var templates = {
    object: '<html>\
    <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# iobjectstory: http://ogp.me/ns/fb/iobjectstory#">\
        <title>%title% - MyCompany</title>\
        <meta property="fb:app_id" content="'+Meteor.util.cnst.fbAppId+'" />\
        <meta property="og:title" content="%title%" />\
        <meta property="og:description" content="%descr%" />\
        <meta property="og:image" content="%photo%" />\
        <meta property="og:url" content="%staticlink%" />\
        <meta property="og:type" content="iobjectstory:object" />\
        <link type="text/css" rel="stylesheet" href="/stylesheets/app.css" />\
    </head>\
    <body>\
      <script type="text/javascript">\
        // javascript redirect for user browser 
        document.location.href = "%link%";\
      </script>\
    </body>\
</html>'
  };
  var ret = templates.object;
  var stlnk = Meteor.util.getObjectStaticPageUrl(object);
  var lnk = Meteor.util.getObjectPageUrl(object);
  ret = ret
    .replace('%title%', object.title)
    .replace('%title%', object.title)
    // compensate for leading slash in stored photo url
    .replace('%photo%', Meteor.absoluteUrl()+object.photo.substr(1))
    .replace('%link%', lnk)
    .replace('%staticlink%', stlnk)
    .replace('%descr%', object.description || '');
  return ret;
}

and, finally, in server/routes.js

Router.map(function() {
    this.route('serverFile', {
        where: 'server',
        path: /^\/static_objects\/(.*)$/,
        action: function() {
            object = Objects.findOne({_id: this.params[0]});
            if (object) {
              var html = '';
              try {
                data = Meteor.util.getStaticObjectPageHtml(object);
                this.response.writeHead(200, {
                    'Content-Type': 'text/html'
                });
                this.response.write(data);
              } catch(e) {
                this.response.writeHead(500, {
                    'Content-Type': 'text/html'
                });
                console.log('error!');
                console.log(e);
              }
              this.response.end();
              return;
            }

           this.response.writeHead(404, {
              'Content-Type': 'text/plain'
           });
           this.response.write('not found: ' + this.params + '\n');
           this.response.end();
        }
    });
});

This is not yet production-ready code, we need to handle some more errors, and i didn’t cover the FB application settings – more on this topic soon.