@mindful-web/marko-web-postup 1.61.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Parameter1 LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/api-client.js ADDED
@@ -0,0 +1,152 @@
1
+ const debug = require('debug')('postup:api');
2
+ const fetch = require('node-fetch');
3
+
4
+ class PostUpApiClient {
5
+ /**
6
+ * @param {string} credentials
7
+ */
8
+ constructor(credentials) {
9
+ if (!credentials) throw new Error('Unable to use the PostUp API without credentials.');
10
+ this.headers = {
11
+ 'content-type': 'application/json',
12
+ authorization: `Basic ${credentials}`,
13
+ };
14
+ }
15
+
16
+ /**
17
+ * @param {string} endpoint
18
+ * @param {object} opts fetch options
19
+ * @returns {Promise<object>}
20
+ */
21
+ async request(endpoint, opts = {}) {
22
+ const method = opts.method || 'post';
23
+ const { headers } = this;
24
+ const url = `https://api.postup.com/api/${endpoint}`;
25
+ const r = await fetch(url, { method, headers, ...opts || {} });
26
+ const response = await r.json();
27
+ const dbg = {
28
+ req: { url, ...{ method, headers, ...opts || {} } },
29
+ res: { headers: r.headers.raw(), body: response },
30
+ };
31
+ if (!r.ok) {
32
+ debug(`${method.toUpperCase()} ${url} ${r.status} ERR`, dbg);
33
+ if (response.message) throw new Error(response.message);
34
+ throw new Error(`API request was unsuccessful: ${r.status} ${r.statusText}`);
35
+ }
36
+ debug(`${method.toUpperCase()} ${url} ${r.status} OK`, dbg);
37
+ return response;
38
+ }
39
+
40
+ /**
41
+ * Creates a recipient and sets idx external id
42
+ * @see https://apidocs.postup.com/docs/subscription-management
43
+ *
44
+ * @param {string} email
45
+ * @param {string} externalId
46
+ * @param {?string} [ip]
47
+ * @returns {Promise}
48
+ */
49
+ async createRecipient(email, externalId, ip) {
50
+ return this.request('recipient', {
51
+ body: JSON.stringify({
52
+ address: email,
53
+ externalId,
54
+ channel: 'E',
55
+ sourceDescription: 'IdentityX',
56
+ signupIP: ip,
57
+ }),
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Retrieves subscription info for the supplied recipient
63
+ * @see https://apidocs.postup.com/docs/return-a-single-recipients-list-subscription
64
+ *
65
+ * @param {string} recipientId
66
+ * @returns {Promise<ListSubscription[]>}
67
+ */
68
+ async listSubscriptionsForRecipient(recipientId) {
69
+ const params = new URLSearchParams({ recipid: recipientId });
70
+ return this.request(`listsubscription?${params}`, { method: 'get' });
71
+ }
72
+
73
+ /**
74
+ * Retrieves a recipient by email address
75
+ * @see https://apidocs.postup.com/docs/retrieve-recipient-data
76
+ *
77
+ * @param {string} email
78
+ * @returns {Promise<Recipient>}
79
+ */
80
+ async returnRecipientDataByEmailAddress(email) {
81
+ const params = new URLSearchParams({ address: email });
82
+ return this.request(`recipient?${params}`, { method: 'get' });
83
+ }
84
+
85
+ /**
86
+ * Updates recipient data (fields, demographics, etc)
87
+ * @see https://apidocs.postup.com/docs/update-existing-recipient-data
88
+ *
89
+ * @param {string} recipientId
90
+ * @param {string} email
91
+ * @param {string[]} demographics
92
+ * @returns {Promise}
93
+ */
94
+ updateRecipient(recipientId, email, demographics) {
95
+ return this.request(`recipient/${recipientId}`, {
96
+ method: 'put',
97
+ body: JSON.stringify({
98
+ address: email,
99
+ demographics,
100
+ }),
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Updates recipient externalId
106
+ * @see https://apidocs.postup.com/docs/update-existing-recipient-data
107
+ *
108
+ * @param {string} recipientId The PostUp recipient id
109
+ * @param {string} externalId The IdentityX user id
110
+ * @returns {Promise}
111
+ */
112
+ updateRecipientExternalId(recipientId, externalId) {
113
+ return this.request(`recipient/${recipientId}`, {
114
+ method: 'put',
115
+ body: JSON.stringify({ externalId }),
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Updates a list subscription for a recipient
121
+ * @see https://apidocs.postup.com/docs/subscribe-recipient-to-a-list
122
+ * @see https://apidocs.postup.com/docs/unsubscribe-recipient-from-a-list
123
+ *
124
+ * @param {string} recipientId
125
+ * @param {string} listId
126
+ * @param {boolean} value
127
+ * @returns {Promise}
128
+ */
129
+ async updateSubscription(recipientId, listId, value) {
130
+ return this.request('listsubscription', {
131
+ body: JSON.stringify({
132
+ listId,
133
+ recipientId,
134
+ status: value ? 'NORMAL' : 'UNSUB',
135
+ }),
136
+ });
137
+ }
138
+ }
139
+
140
+ module.exports = PostUpApiClient;
141
+
142
+ /**
143
+ * @typedef ListSubscription
144
+ * @prop {Number} recipientId
145
+ * @prop {Number} listId
146
+ * @prop {ListSubscriptionStatus} status
147
+ * @prop {ListSubscriptionStatus} listStatus
148
+ * @prop {ListSubscriptionStatus} globalStatus
149
+ * @prop {Boolean} confirmed
150
+ *
151
+ * @typedef {("NORMAL"|"UNSUB")} ListSubscriptionStatus
152
+ */
package/env.js ADDED
@@ -0,0 +1,7 @@
1
+ const { cleanEnv, validators } = require('@mindful-web/env');
2
+
3
+ const { nonemptystr } = validators;
4
+
5
+ module.exports = cleanEnv(process.env, {
6
+ POSTUP_API_CREDENTIALS: nonemptystr({ desc: 'The PostUp API credentials.' }),
7
+ });
package/hooks/index.js ADDED
@@ -0,0 +1,51 @@
1
+ const onAuthenticationSuccess = require('./on-authentication-success');
2
+ const onLoginLinkSent = require('./on-login-link-sent');
3
+ const onUserProfileUpdate = require('./on-user-profile-update');
4
+
5
+ /**
6
+ *
7
+ * @param {import('../index').IdentityXConfiguration} idxConfig
8
+ * @param {PostUpService} postUp
9
+ */
10
+ module.exports = (idxConfig, postUp) => {
11
+ if (idxConfig.postupInstalled) return;
12
+ idxConfig.addHook({
13
+ name: 'onUserProfileUpdate',
14
+ shouldAwait: false,
15
+ fn: (args) => onUserProfileUpdate({ postUp, ...args }),
16
+ });
17
+
18
+ idxConfig.addHook({
19
+ name: 'onLoginLinkSent',
20
+ shouldAwait: false,
21
+ fn: (args) => onLoginLinkSent({ postUp, ...args }),
22
+ });
23
+
24
+ idxConfig.addHook({
25
+ name: 'onAuthenticationSuccess',
26
+ shouldAwait: true,
27
+ fn: (args) => onAuthenticationSuccess({ postUp, ...args }),
28
+ });
29
+ // eslint-disable-next-line no-param-reassign
30
+ idxConfig.postupInstalled = true;
31
+ };
32
+
33
+ /**
34
+ * @typedef {import('@mindful-web/marko-web-identity-x/service')} IdentityXService
35
+ * @typedef {import('../service')} PostUpService
36
+ * @typedef {import('../index').PostUpConfig} PostUpConfig
37
+ * @typedef {import('../index').PostUpRequest} PostUpRequest
38
+ *
39
+ * @typedef IdentityXUser
40
+ * @prop {String} id
41
+ * @prop {String} email
42
+ * @prop {import('@mindful-web/marko-web-identity-x/service').ExternalId[]} externalIds
43
+ *
44
+ * @typedef IdentityXHookArgs
45
+ * @prop {Object} additionalEventData
46
+ * @prop {Object} authToken
47
+ * @prop {PostUpService} postUp
48
+ * @prop {IdentityXService} service
49
+ * @prop {IdentityXUser} user
50
+ * @prop {String} loginSource
51
+ * */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @param {import("./index").IdentityXHookArgs} args
3
+ */
4
+ module.exports = async ({
5
+ // loginSource,
6
+ postUp,
7
+ service,
8
+ user,
9
+ }) => {
10
+ await postUp.ensureUser(user, service);
11
+
12
+ // Auto-subscribe the user if this is the first successful authentication
13
+ // if (loginSource === 'newsletterSignup' && user.verifiedCount === 1) {
14
+ // await postUp.defaultListOptIns(service, user);
15
+ // }
16
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @param {import("./index").IdentityXHookArgs} args
3
+ */
4
+ module.exports = async ({
5
+ // additionalEventData,
6
+ postUp,
7
+ service,
8
+ user,
9
+ }) => {
10
+ // Attempt to import user data to IdentityX
11
+ if (user && !user.verified) {
12
+ await postUp.updateIdentityXData(user, service);
13
+ await postUp.defaultListOptIns(service, user);
14
+ }
15
+
16
+ // If the user has already verified and is coming from nl signup, sign them up to the default list
17
+ // if (user && user.verified && additionalEventData.actionSource === 'newsletterSignup') {
18
+ // await postUp.defaultListOptIns(service, user);
19
+ // }
20
+
21
+ return user;
22
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @param {import("./index").IdentityXHookArgs} args
3
+ */
4
+ module.exports = async ({
5
+ // additionalEventData,
6
+ postUp,
7
+ user,
8
+ service,
9
+ }) => {
10
+ await postUp.ensureUser(user, service);
11
+
12
+ await postUp.updatePostUpData(user, service);
13
+
14
+ return user;
15
+ };
package/index.js ADDED
@@ -0,0 +1,72 @@
1
+ const { get } = require('@mindful-web/object-path');
2
+
3
+ /* eslint-disable max-len */
4
+ const Joi = require('@parameter1/joi');
5
+ const { validate } = require('@parameter1/joi/utils');
6
+ const PostUp = require('./service');
7
+ const hooks = require('./hooks');
8
+
9
+ /**
10
+ * @param {import("express").Application} app
11
+ * @param {IdentityXConfiguration} idxConfig
12
+ * @param {PostUpConfig} params
13
+ *
14
+ * @returns {PostUpConfig}
15
+ */
16
+
17
+ module.exports = (app, idxConfig, params = {}) => {
18
+ const args = validate(Joi.object({
19
+ defaultListQuestionIds: Joi.array().items(Joi.string().pattern(/[a-f0-9]{24}/i)).required(),
20
+ fieldMap: Joi.object().default({
21
+ // PostUp standard fields
22
+ FirstName: 'givenName',
23
+ LastName: 'familyName',
24
+ _Address1: 'street',
25
+ _Address2: 'addressExtra',
26
+ _City: 'city',
27
+ _Country: 'countryCode',
28
+ _CountryCode: 'countryCode',
29
+ _HomePhone: 'phoneNumber',
30
+ _PostalCode: 'postalCode',
31
+ _State: 'regionCode',
32
+ // PostUp custom fields
33
+ companyname: 'organization',
34
+ title: 'organizationTitle',
35
+ }).description('The PostUp fields that should be mapped to IdentityX fields.'),
36
+ }), params);
37
+
38
+ app.use((req, res, next) => {
39
+ // Install service
40
+ const service = new PostUp({ ...args, cookies: req.cookies || {} });
41
+ req.PostUp = service;
42
+ res.locals.PostUp = service;
43
+
44
+ // Install custom handler for content metering
45
+ // eslint-disable-next-line no-param-reassign
46
+ app.locals.contentBypassGatingHandler = ({ content }) => {
47
+ // Skip metering if we already have an IdX identity
48
+ const idt = req.identityX.getIdentity(res);
49
+ if (idt) return true;
50
+
51
+ // Skip metering if we have a constant contact email param
52
+ if (req.query.pu_ext_id) return true;
53
+
54
+ // Fall back to default behavior
55
+ return get(content, 'userRegistration.bypassGating', false);
56
+ };
57
+
58
+ // Install hooks
59
+ hooks(idxConfig, service);
60
+ next();
61
+ });
62
+ };
63
+
64
+ /**
65
+ * @typedef {import('@mindful-web/marko-web-identity-x/config')} IdentityXConfiguration
66
+ * @typedef PostUpConfig
67
+ * @prop {Array} defaultListQuestionIds Array of object ids representing the default subscription questions
68
+ * @prop {Object} fieldMap An object representing how fields should map from PostUp to IdentityX.
69
+ *
70
+ * @typedef PostUpRequest
71
+ * @prop {PostUp} postUp
72
+ */
@@ -0,0 +1,27 @@
1
+ const { asyncRoute } = require('@mindful-web/utils');
2
+
3
+ /**
4
+ * @typedef OIDXRequest
5
+ * @prop {import('@mindful-web/marko-web-identity-x/service')} identityX
6
+ *
7
+ * @typedef MiddlewareConstructor
8
+ */
9
+ module.exports = asyncRoute(async (req, res, next) => {
10
+ /** @type {OIDXRequest} */
11
+ const { identityX: idx } = req;
12
+ const cookie = idx.getIdentity(res);
13
+
14
+ // Don't overwrite an existing cookie
15
+ if (cookie) return next();
16
+
17
+ // get oly enc id. if we don't have one, bail
18
+ const postUpId = req.query.pu_ext_id;
19
+ if (!postUpId) return next();
20
+
21
+ // Look up idx user by pu_ext_id or our idx mongoID
22
+ const identity = await idx.findUserById(postUpId);
23
+ if (identity) {
24
+ idx.setIdentityCookie(identity.id);
25
+ }
26
+ return next();
27
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mindful-web/marko-web-postup",
3
+ "version": "1.61.3",
4
+ "author": "Josh Worden <josh@parameter1.com>",
5
+ "repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-postup",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "lint": "eslint --ext .js --ext .vue --max-warnings 5 --config ../../.eslintrc.js --ignore-path ../../.eslintignore ./",
9
+ "lint:fix": "yarn lint --fix",
10
+ "test": "yarn lint"
11
+ },
12
+ "dependencies": {
13
+ "@mindful-web/env": "^1.0.0",
14
+ "@mindful-web/marko-web-identity-x": "^1.60.0",
15
+ "@mindful-web/object-path": "^1.57.0",
16
+ "@mindful-web/utils": "^1.57.0",
17
+ "@parameter1/joi": "^1.2.10",
18
+ "debug": "^4.1.1",
19
+ "express": "^4.17.1",
20
+ "graphql-tag": "^2.12.6",
21
+ "newrelic": "^9.10.2",
22
+ "node-fetch": "^2.6.1"
23
+ },
24
+ "engines": {
25
+ "node": ">=14.15"
26
+ },
27
+ "os": [
28
+ "darwin",
29
+ "linux",
30
+ "win32"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "gitHead": "713b78883a51d0bcce2e82d84cf361353fa4823a"
36
+ }
package/service.js ADDED
@@ -0,0 +1,339 @@
1
+ const debug = require('debug')('postup:service');
2
+ const gql = require('graphql-tag');
3
+ const { get, getAsArray } = require('@mindful-web/object-path');
4
+ const { POSTUP_API_CREDENTIALS } = require('./env');
5
+ const { filterByExternalId, filterFieldsByExternalId } = require('./utils');
6
+ const PostUpApiClient = require('./api-client');
7
+
8
+ const SET_DATA = gql`
9
+ mutation SetPostUpData($input: SetAppUserUnverifiedDataMutationInput!) {
10
+ setAppUserUnverifiedData(input: $input) { id }
11
+ }
12
+ `;
13
+
14
+ const SET_SELECT_FIELD_ANSWERS = gql`
15
+ mutation SetPostUpSelectFieldAnswers($input: UpdateAppUserCustomSelectAnswersMutationInput!) {
16
+ updateAppUserCustomSelectAnswers(input: $input) { id }
17
+ }
18
+ `;
19
+
20
+ const SET_BOOLEAN_FIELD_ANSWERS = gql`
21
+ mutation SetPostUpBooleanFieldAnswers($input: UpdateAppUserCustomBooleanAnswersMutationInput!) {
22
+ updateAppUserCustomBooleanAnswers(input: $input) { id }
23
+ }
24
+ `;
25
+
26
+ class PostUp {
27
+ /**
28
+ * @param {string} defaultListQuestionIds The array of default lists for auto-signups
29
+ * @param {object} fieldMap A mapping of postup>identityx field keys
30
+ */
31
+ constructor({
32
+ defaultListQuestionIds,
33
+ fieldMap = {},
34
+ tenant = 'transpire',
35
+ } = {}) {
36
+ this.defaultListQuestionIds = defaultListQuestionIds;
37
+ this.fieldMap = Object.entries(fieldMap).reduce((map, [key, value]) => {
38
+ map.set(key, value);
39
+ return map;
40
+ }, new Map());
41
+ this.tenant = tenant;
42
+ this.client = new PostUpApiClient(POSTUP_API_CREDENTIALS);
43
+ }
44
+
45
+ /**
46
+ * Writes PostUp-supplied user data to IdentityX
47
+ *
48
+ * @param {import('./hooks').IdentityXUser} user
49
+ * @param {IdentityXService} idx The IdentityX user service
50
+ * @returns {Promise<any>}
51
+ */
52
+ async updateIdentityXData(user, idx) {
53
+ // Don't overwrite verified user data
54
+ if (user.verified) return;
55
+
56
+ const [userData = {}] = await this.getUserByEmail(user.email);
57
+ if (!userData || !userData.recipientId) {
58
+ await this.ensureUser(user, idx);
59
+ return;
60
+ }
61
+
62
+ const { demographics = [], recipientId } = userData;
63
+
64
+ debug('updateIdentityXData.externalId', recipientId);
65
+ await this.setExternalIds(idx, user.id, recipientId);
66
+
67
+ const demoData = demographics.reduce((o, kv) => {
68
+ const [k, v] = kv.split('=');
69
+ return { ...o, [k]: v };
70
+ }, {});
71
+
72
+ // Update root fields
73
+ const payload = Object.entries(demoData).reduce((o, [k, v]) => ({
74
+ ...o,
75
+ ...(this.fieldMap.has(k) && v && {
76
+ [this.fieldMap.get(k)]: v,
77
+ }),
78
+ }), { email: user.email });
79
+
80
+ debug('updateIdentityXData.data', payload);
81
+ await idx.client.mutate({
82
+ context: { apiToken: idx.getOrgUserApiToken() },
83
+ mutation: SET_DATA,
84
+ variables: { input: payload },
85
+ });
86
+
87
+ // Update custom selects
88
+ const questions = filterFieldsByExternalId(getAsArray(user, 'customSelectFieldAnswers'), 'demographic');
89
+ const customSelectFieldAnswers = questions.reduce((arr, ans) => {
90
+ // Don't overwrite previously set answers
91
+ if (!ans.hasAnswered) {
92
+ const key = `${get(ans, 'field.externalId.identifier.value')}`.toLocaleLowerCase();
93
+ const { id: fieldId } = ans.field;
94
+ if (demoData[key]) {
95
+ // find the answer, set the value
96
+ const found = ans.field.options.find((opt) => opt.externalIdentifier === demoData[key]);
97
+ if (found) arr.push({ fieldId, optionIds: [found.id] });
98
+ }
99
+ }
100
+ return arr;
101
+ }, []);
102
+
103
+ debug('updateIdentityXData.selects', customSelectFieldAnswers);
104
+ if (customSelectFieldAnswers.length) {
105
+ await idx.client.mutate({
106
+ context: { apiToken: idx.getOrgUserApiToken() },
107
+ mutation: SET_SELECT_FIELD_ANSWERS,
108
+ variables: {
109
+ input: {
110
+ id: user.id,
111
+ answers: customSelectFieldAnswers,
112
+ },
113
+ },
114
+ });
115
+ }
116
+
117
+ const subs = await this.client.listSubscriptionsForRecipient(recipientId);
118
+
119
+ const subscriptions = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
120
+ const customBooleanFieldAnswers = subscriptions.reduce((arr, ans) => {
121
+ const listId = get(ans, 'field.externalId.identifier.value');
122
+ const match = subs.find((sub) => sub.listId === parseInt(listId, 10));
123
+ if (match) arr.push({ fieldId: ans.field.id, value: match.status === 'NORMAL' });
124
+ return arr;
125
+ }, []);
126
+
127
+ debug('updateIdentityXData.bools', customBooleanFieldAnswers);
128
+ if (customBooleanFieldAnswers.length) {
129
+ await idx.client.mutate({
130
+ context: { apiToken: idx.getOrgUserApiToken() },
131
+ mutation: SET_BOOLEAN_FIELD_ANSWERS,
132
+ variables: {
133
+ input: {
134
+ id: user.id,
135
+ answers: customBooleanFieldAnswers,
136
+ },
137
+ },
138
+ });
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Writes user-supplied IdentityX data to PostUp
144
+ *
145
+ * @param {import('./hooks').IdentityXUser} user
146
+ * @returns {Promise<any>}
147
+ */
148
+ async updatePostUpData(user) {
149
+ const demographics = [...this.fieldMap.entries()].reduce((o, [p, i]) => ({
150
+ ...o,
151
+ [p]: get(user, i),
152
+ }), {});
153
+
154
+ // External ID tagged questions
155
+ const questions = filterFieldsByExternalId(getAsArray(user, 'customSelectFieldAnswers'), 'demographic');
156
+ questions.forEach((ans) => {
157
+ if (ans.hasAnswered) {
158
+ const key = get(ans, 'field.externalId.identifier.value');
159
+ const answers = getAsArray(ans, 'answers').map((a) => a.writeInValue || a.externalIdentifier);
160
+ demographics[key] = ans.field.multiple ? answers.join(',') : answers.pop();
161
+ }
162
+ });
163
+
164
+ // Update Postup with the user data and demographics
165
+ await this.updateRecipient(user, demographics);
166
+
167
+ const answers = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
168
+ const subscriptions = answers.reduce((map, answer) => {
169
+ const id = get(answer, 'field.externalId.identifier.value');
170
+ map.set(`${id}`, answer.value);
171
+ return map;
172
+ }, new Map());
173
+
174
+ // Update Postup with user subscriptions
175
+ await this.updateSubscriptions(user, subscriptions);
176
+ }
177
+
178
+ /**
179
+ * Ensure the user has been sent to postup.
180
+ *
181
+ * @param {import('./hooks').IdentityXUser} user
182
+ * @param {IdentityXService} idx The IdentityX user service
183
+ * @returns {Promise<any>}
184
+ */
185
+ async ensureUser(user, idx) {
186
+ // 1. Check IdentityX user externalIds for postup recipient id, bail if found
187
+ const found = filterByExternalId(user.externalIds, 'recipient');
188
+ if (found && found.length) return user;
189
+
190
+ // 2. Attempt to look up by email address. If found, push externalId to IdX/PostUp users
191
+ const data = await this.getUserByEmail(user.email);
192
+ const recipientId = get(data, '0.recipientId');
193
+ if (recipientId) return this.setExternalIds(idx, user.id, recipientId);
194
+
195
+ // 3. Create user. Push externalId to IdX user
196
+ return this.createRecipient(user.email, user.id, idx);
197
+ }
198
+
199
+ /**
200
+ * Sets the IdentityX externalId
201
+ *
202
+ * @param {IdentityXService} idx The IdentityX user service
203
+ * @param {string} userId The IdentityX user id
204
+ * @param {string} externalId The PostUp recipient id
205
+ * @return {Promise}
206
+ */
207
+ async setExternalIds(idx, userId, externalId) {
208
+ return Promise.all([
209
+ idx.addExternalUserId({
210
+ userId,
211
+ identifier: { value: `${externalId}` },
212
+ namespace: { provider: 'postup', tenant: this.tenant, type: 'recipient' },
213
+ }),
214
+ this.client.updateRecipientExternalId(externalId, userId),
215
+ ]);
216
+ }
217
+
218
+ /**
219
+ * Subscribes a user to the default list.
220
+ *
221
+ * @param {IdentityXService} idx The IdentityX user service
222
+ * @param {import('./hooks').IdentityXUser} user The IdentityX user object
223
+ */
224
+ async defaultListOptIns(idx, user) {
225
+ const subscriptions = filterFieldsByExternalId(getAsArray(user, 'customBooleanFieldAnswers'), 'list');
226
+
227
+ const recipientId = await this.getRecipientId(user);
228
+
229
+ const answers = new Map();
230
+ const postUpSupscritions = subscriptions.reduce((map, a) => {
231
+ const id = get(a, 'field.id');
232
+ const externalId = get(a, 'field.externalId.identifier.value');
233
+ if (this.defaultListQuestionIds.includes(id)) {
234
+ map.set(`${externalId}`, true);
235
+ answers.set(`${id}`, { fieldId: id, value: true });
236
+ }
237
+ return map;
238
+ }, new Map());
239
+
240
+ debug('defaultListOptIn', {
241
+ user: user.id,
242
+ recipientId,
243
+ postUpSupscritions,
244
+ answers: [...answers.values()],
245
+ });
246
+ await Promise.all([
247
+ // Update Postup with user subscriptions by default
248
+ await this.updateSubscriptions(user, postUpSupscritions),
249
+ // Set question value in IdentityX
250
+ idx.client.mutate({
251
+ context: { apiToken: idx.getOrgUserApiToken() },
252
+ mutation: SET_BOOLEAN_FIELD_ANSWERS,
253
+ variables: {
254
+ input: {
255
+ id: user.id,
256
+ answers: [...answers.values()],
257
+ },
258
+ },
259
+ }),
260
+ ]);
261
+ }
262
+
263
+ /**
264
+ * Returns the PostUp id for the supplied user
265
+ *
266
+ * @param {import('./hooks').IdentityXUser} user
267
+ * @returns {Promise<string>}
268
+ */
269
+ async getRecipientId(user) {
270
+ const { email } = user;
271
+ const extIds = filterByExternalId(user.externalIds, 'recipient');
272
+ const extId = get(extIds, '0.identifier.value');
273
+ if (extId) return extId;
274
+
275
+ const data = await this.getUserByEmail(email);
276
+ const found = get(data, '0.recipientId');
277
+ if (found) return found;
278
+
279
+ throw new Error(`Unable to find a recipient id for IdentityX user "${user.id}".`);
280
+ }
281
+
282
+ /**
283
+ * Updates recipient data (fields, demographics, etc)
284
+ *
285
+ * @param {import('./hooks').IdentityXUser} user
286
+ * @param {object} demographics
287
+ * @returns {Promise<any>}
288
+ */
289
+ async updateRecipient(user, demographics) {
290
+ const { email } = user;
291
+ const formatted = Object.keys(demographics).reduce((arr, k) => {
292
+ arr.push(`${k}=${demographics[k]}`);
293
+ return arr;
294
+ }, []);
295
+ const recipientId = await this.getRecipientId(user);
296
+ return this.client.updateRecipient(recipientId, email, formatted);
297
+ }
298
+
299
+ /**
300
+ * @param {import('./hooks').IdentityXUser} user
301
+ * @param {Map<string, boolean>} subscriptions
302
+ */
303
+ async updateSubscriptions(user, subscriptions) {
304
+ const recipientId = await this.getRecipientId(user);
305
+ return Promise.all(
306
+ [...subscriptions].map(async ([id, v]) => this.client.updateSubscription(recipientId, id, v)),
307
+ );
308
+ }
309
+
310
+ /**
311
+ * Retrieves a recipient by email address
312
+ *
313
+ * @param {string} email
314
+ * @returns {Promise<Recipient>}
315
+ */
316
+ async getUserByEmail(email) {
317
+ return this.client.returnRecipientDataByEmailAddress(email);
318
+ }
319
+
320
+ /**
321
+ * Creates a recipient and sets idx external id
322
+ *
323
+ * @param {string} email
324
+ * @param {string} externalId
325
+ * @param {IdentityXService} idx
326
+ * @returns {Promise}
327
+ */
328
+ async createRecipient(email, externalId, idx) {
329
+ const ip = idx.req.header('cf-connecting-ip') || idx.req.ip;
330
+ const created = await this.client.createRecipient(email, externalId, ip);
331
+ return this.setExternalIds(idx, externalId, `${created.recipientId}`);
332
+ }
333
+ }
334
+
335
+ module.exports = PostUp;
336
+
337
+ /**
338
+ * @typedef {import("@mindful-web/marko-web-identity-x/service")} IdentityXService
339
+ */
package/utils.js ADDED
@@ -0,0 +1,12 @@
1
+ const { getAsObject } = require('@mindful-web/object-path');
2
+
3
+ module.exports = {
4
+ filterByExternalId: (arr, type) => arr.filter((v) => {
5
+ const ns = getAsObject(v, 'namespace');
6
+ return ns.provider === 'postup' && ns.type === type && ns.tenant === 'transpire';
7
+ }),
8
+ filterFieldsByExternalId: (arr, type) => arr.filter((v) => {
9
+ const ns = getAsObject(v, 'field.externalId.namespace');
10
+ return ns.provider === 'postup' && ns.type === type && ns.tenant === 'transpire';
11
+ }),
12
+ };