How do I create a registration interstitial?
-
I have added custom fields to the registration form using
filter:register.build
. But I would like to move those fields to the GDPR page where theemail
field is at. How can I achieve this? Is there a similar hook I can use?Also, do I need to use different hooks for any of the following?
{ "hook": "filter:register.build", "method": "addField" }, { "hook": "filter:register.check", "method": "checkField" }, { "hook": "filter:user.getRegistrationQueue", "method": "customFields" }, { "hook": "filter:user.create", "method": "creatingUser" }, { "hook": "action:user.create", "method": "createdUser" }, { "hook": "filter:user.addToApprovalQueue", "method": "addToApprovalQueue" }
-
-
The
.email()
interstitial is the most confusing and has many different code paths (based on whether the caller is an admin, a regular user, a guest, whether they're modifying their email, etc...)Simplest would be to copy
.gdpr()
, which are two simple checkboxes, and adapt it to your needs.Here it is broken down with comments:
// if the user already consented or GDPR is disabled, return early. data.userData contains the contents of req.session. if (!meta.config.gdpr_enabled || (data.userData && data.userData.gdpr_consent)) { return data; } // if there is no user data (null case check) if (!data.userData) { throw new Error('Invalid Data'); } // if data.userData.uid is present it means this is an EXISTING user, not a new user. Check their hash to see whether they consented. if (data.userData.uid) { const consented = await db.getObjectField(`user:${data.userData.uid}`, 'gdpr_consent'); if (parseInt(consented, 10)) { return data; } } // this is the end of the "early returns". If we are here it means the user did not consent yet, and needs the interstitial data.interstitials.push({ template: 'partials/gdpr_consent', // hopefully self-explanatory? data: { // the data that is passed to the above template digestFrequency: meta.config.dailyDigestFreq, digestEnabled: meta.config.dailyDigestFreq !== 'off', }, // called when the form is submitted. userData is req.session, formData is the serialized form data in object format. Do value checks here and set the value in userData. It is checked at the top of this code block, remember? callback: function (userData, formData, next) { if (formData.gdpr_agree_data === 'on' && formData.gdpr_agree_email === 'on') { userData.gdpr_consent = true; } // throw an error if the user didn't check the box. You can pass a language key here, or just plain text. The end user will have the page reloaded and your error will be shown. next(userData.gdpr_consent ? null : new Error('You must give consent to this site to collect/process your information, and to send you emails.')); }, }); return data;
-
That hook would be
filter:registerComplete.build
The hook name is derived from the template passed tores.render
in this case it isres.render('registerComplete', data)
https://github.com/NodeBB/NodeBB/blob/master/src/controllers/index.js#L211 -
-
@baris, thanks, but it seems the
registerComplete
page does not automatically handle fields that are added to the page like theregister
page does. It only handles the email field in User.acceptRegistration.So what is the best way to go to achieve how the
register
page handles custom whitelisted fields added, but instead in theregisterComplete
page?The main reason to move from the
register
page to theregisterComplete
page is so that users are able to use Google Oauth and still be asked to answer some additional questions as part of the registration process. -
-
I tried the
filter:register.interstitial
hook. It does get triggered on bothregister
andregisterComplete
. -
This is my plugin code for the hook
filter:register.interstitial
plugin.registerInterstitial = function (params) { const url = params.req.originalUrl; console.log("url", URL); var customInterstital = { template: "partials/customRegistration", data: { test: "test from customInterstital", }, callback: async (userData, formData) => { console.log("customInterstital callback"); console.log(userData); console.log(formData); userData.test = formData.test; }, }; params.interstitials.unshift(customInterstital); return params; };
@baris @julian, when I try to test this, the logs show me the following warning: Interstitial callbacks processed with no errors, but one or more interstitials remain. This is likely an issue with one of the interstitials not properly handling a null case or invalid value.
Also, the other interstitials (email, gdpr) disappear but the custom one I made still stays on the screen. The devtools logs also shows this:
Error: Cannot find module './registerComplete'
What am I doing wrong?
-
@julian, please share your insights on this.
-
@saikarthikp9 The dev tools warning is expected and can be ignored, there is no custom js for that particular page
The server-side error you see is a hint that your interstitials were not constructed correctly. The easiest way to construct one is to reference one that is already built, such as the ones in
src/user/interstitials.js
In your callback, you are passed a single parameter, that contains
{ req, userData, interstitials }
, you need to readuserData
to determine whether or not to show your interstitial.If you do, you need to push an object to
data.interstitials
, including acallback
, which is what will be called when the interstitial form is submitted. If everything is good in the passed informData
, then you need to make a change somewhere (likedata.req.session
or saving to the user hash, etc.), so that when the hook is called again, the interstitial will not be shown. -
@saikarthikp9 I will admit that the registration interstitial system is confusing. It was confusing when I wrote it, and @baris said as such. My goal was to create an open-ended system to allow people to create their own forms for capturing additional data, and while this was achieved, it is not the easiest to write the interstitial logic.
It is on our running (very long) list of things to refactor.
Nevertheless, existing implementations would still be supported for awhile (just deprecated), so it should still be safe to write them.
-
The
.email()
interstitial is the most confusing and has many different code paths (based on whether the caller is an admin, a regular user, a guest, whether they're modifying their email, etc...)Simplest would be to copy
.gdpr()
, which are two simple checkboxes, and adapt it to your needs.Here it is broken down with comments:
// if the user already consented or GDPR is disabled, return early. data.userData contains the contents of req.session. if (!meta.config.gdpr_enabled || (data.userData && data.userData.gdpr_consent)) { return data; } // if there is no user data (null case check) if (!data.userData) { throw new Error('Invalid Data'); } // if data.userData.uid is present it means this is an EXISTING user, not a new user. Check their hash to see whether they consented. if (data.userData.uid) { const consented = await db.getObjectField(`user:${data.userData.uid}`, 'gdpr_consent'); if (parseInt(consented, 10)) { return data; } } // this is the end of the "early returns". If we are here it means the user did not consent yet, and needs the interstitial data.interstitials.push({ template: 'partials/gdpr_consent', // hopefully self-explanatory? data: { // the data that is passed to the above template digestFrequency: meta.config.dailyDigestFreq, digestEnabled: meta.config.dailyDigestFreq !== 'off', }, // called when the form is submitted. userData is req.session, formData is the serialized form data in object format. Do value checks here and set the value in userData. It is checked at the top of this code block, remember? callback: function (userData, formData, next) { if (formData.gdpr_agree_data === 'on' && formData.gdpr_agree_email === 'on') { userData.gdpr_consent = true; } // throw an error if the user didn't check the box. You can pass a language key here, or just plain text. The end user will have the page reloaded and your error will be shown. next(userData.gdpr_consent ? null : new Error('You must give consent to this site to collect/process your information, and to send you emails.')); }, }); return data;
-
-
-
@julian for some clarity, please help me with the following:
-
Since
filter:register.interstitial
is fired on bothregister
andregisterComplete
, we can ignore the hook fired byregister
by checking ifdata.req.originalUrl
starts with/register/complete
and return early if it does NOT. Right? -
When calling the
next()
function in the callback with an error, is there a way to send a list of errors rather than only 1 message likenext(new Error("Error"));
? -
The following check is not required if we do not plan to update interstitials in the future, right?
@julian said in filter:register.build equivalent for the GDPR page (/register/complete)?:
// if data.userData.uid is present it means this is an EXISTING user, not a new user. Check their hash to see whether they consented. -
-
-
The hook is fired on
POST /register
so that the user can be redirected to the interstitial during the registration process. So you do need to have it respond there in order for the flow to work. -
Errors are collected and displayed in a list, but each interstitial can throw only one error. Limitation of the implementation unfortunately.
-
I think this is correct, yes...
-
-
@julian said in How do I create a registration interstitial?:
The hook is fired on POST /register so that the user can be redirected to the interstitial during the registration process. So you do need to have it respond there in order for the flow to work.
Interesting, the flow still seems to work perfectly (the custom interstitials do show up on redirect to
/register/complete
) with the following code right at the beginning of the hook handler:const url = data.req.originalUrl; if (!url.startsWith("/register/complete")) { return data; }
How is that possible?
-