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.